happyskills 1.12.0 → 1.12.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,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.12.1] - 2026-06-23
11
+
12
+ ### Changed
13
+ - On a rate-limit `429`, steer anonymous callers to sign in instead of just retrying. When the API flags an anonymous trip on the search/resolve paths (`error.details.login_helps`), the CLI prints "You've hit the anonymous rate limit. Sign in or create a free account for a much higher limit — run `happyskills login`" (rather than a bare "wait and retry"), and `--json` mode emits a `login` `next_step`. Authenticated and `/auth/*` trips are unchanged. (Pairs with API `api-v5.15.3`.)
14
+
10
15
  ## [1.12.0] - 2026-06-22
11
16
 
12
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "1.12.0",
3
+ "version": "1.12.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/client.js CHANGED
@@ -100,12 +100,22 @@ const request = (method, path, options = {}) => catch_errors(`API ${method} ${pa
100
100
 
101
101
  if (res.status === 429) {
102
102
  const retry_after = res.headers.get('retry-after')
103
- const retry_msg = retry_after
104
- ? `API rate limit reached. Please wait ${retry_after} seconds and try again.`
105
- : 'API rate limit reached. Please wait and try again.'
103
+ // The API sets login_helps when the caller is anonymous on a path where
104
+ // signing in lifts the cap (search/resolve) turn the dead-end into a
105
+ // sign-up nudge instead of a bare "wait and retry".
106
+ const login_helps = !!(err_details && err_details.login_helps)
107
+ const wait_hint = retry_after ? ` (or wait ${retry_after}s and retry)` : ''
108
+ const retry_msg = login_helps
109
+ ? `You've hit the anonymous rate limit. Sign in or create a free account for a much higher limit — run \`happyskills login\`${wait_hint}.`
110
+ : (retry_after
111
+ ? `API rate limit reached. Please wait ${retry_after} seconds and try again.`
112
+ : 'API rate limit reached. Please wait and try again.')
106
113
  const err = new ApiError(retry_msg, 429, 'RATE_LIMITED')
107
114
  err.retry_after = retry_after ? parseInt(retry_after) : null
108
115
  if (err_details) err.details = { ...err.details, ...err_details }
116
+ // The JSON-mode marshaller builds next_step from err.context (not details),
117
+ // so stash the branch inputs there to get the LOGIN vs RETRY next_step.
118
+ err.context = { login_helps, ...(retry_after ? { retry_after_seconds: parseInt(retry_after) } : {}) }
109
119
  throw err
110
120
  }
111
121
 
@@ -74,11 +74,24 @@ const NEXT_STEP_BY_ERROR_CODE = Object.freeze({
74
74
  'A network error interrupted the request. Retry shortly.',
75
75
  { retry_after_seconds: 5, max_attempts: 3 }
76
76
  ),
77
- RATE_LIMITED: (msg, ctx = {}) => recovery(
78
- RETRY,
79
- 'Rate limit reached. Retry after the suggested interval.',
80
- { retry_after_seconds: ctx.retry_after_seconds || 30, max_attempts: 3 }
81
- ),
77
+ // An anonymous caller throttled on a per-IP bucket gets a much higher cap (and
78
+ // their own per-user budget) once signed in, so when the API flags `login_helps`
79
+ // we steer them to authenticate instead of just retrying. Authenticated trips
80
+ // (and the pre-login /auth/* limiter) fall through to the plain retry recovery.
81
+ RATE_LIMITED: (_msg, ctx = {}) => ctx.login_helps
82
+ ? recovery(
83
+ LOGIN,
84
+ 'You hit the anonymous rate limit. Sign in or create a free account for a much higher limit, then re-run.',
85
+ {
86
+ retry_after_seconds: ctx.retry_after_seconds || 30,
87
+ commands: ['npx happyskills login --browser --json'],
88
+ }
89
+ )
90
+ : recovery(
91
+ RETRY,
92
+ 'Rate limit reached. Retry after the suggested interval.',
93
+ { retry_after_seconds: ctx.retry_after_seconds || 30, max_attempts: 3 }
94
+ ),
82
95
  DB_UNAVAILABLE: () => recovery(
83
96
  RETRY,
84
97
  'The registry database is temporarily unavailable. Retry shortly.',
@@ -7,7 +7,7 @@
7
7
  const { describe, it } = require('node:test')
8
8
  const assert = require('node:assert/strict')
9
9
  const { next_step_for_error } = require('./next_step_by_error_code')
10
- const { DISCOVER_SCHEMA, ROUTING, kind_for_action } = require('./next_step_actions')
10
+ const { DISCOVER_SCHEMA, ROUTING, RECOVERY, LOGIN, RETRY, kind_for_action } = require('./next_step_actions')
11
11
 
12
12
  describe('next_step_by_error_code — schema discovery routing', () => {
13
13
  it('discover_schema is a routing-kind action', () => {
@@ -28,3 +28,19 @@ describe('next_step_by_error_code — schema discovery routing', () => {
28
28
  })
29
29
  }
30
30
  })
31
+
32
+ describe('next_step_by_error_code — RATE_LIMITED steers anonymous callers to sign in', () => {
33
+ it('login_helps → a LOGIN recovery that points at `happyskills login`', () => {
34
+ const ns = next_step_for_error('RATE_LIMITED', 'Too many requests.', { login_helps: true, retry_after_seconds: 42 })
35
+ assert.strictEqual(ns.kind, RECOVERY)
36
+ assert.strictEqual(ns.action, LOGIN)
37
+ assert.ok(ns.context.commands.some(c => c.includes('login')), 'must point at the login command')
38
+ assert.strictEqual(ns.context.retry_after_seconds, 42)
39
+ })
40
+
41
+ it('without login_helps → a plain RETRY recovery (authenticated / auth-path trips)', () => {
42
+ const ns = next_step_for_error('RATE_LIMITED', 'Too many requests.', { retry_after_seconds: 30 })
43
+ assert.strictEqual(ns.kind, RECOVERY)
44
+ assert.strictEqual(ns.action, RETRY)
45
+ })
46
+ })