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
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
})
|