@zenrows/cli 1.9.0 → 1.10.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/README.md +17 -4
- package/agent-plugin/skills/interact-browser/SKILL.md +4 -2
- package/agent-plugin/skills/zenrows/SKILL.md +4 -1
- package/dist/src/cli/commands/signup.js +42 -7
- package/dist/src/core/agent-account.js +46 -6
- package/dist/src/core/config.js +1 -1
- package/dist/src/core/ensure-key.js +11 -5
- package/dist/src/core/errors.js +3 -0
- package/dist/src/core/signup-challenge.js +114 -0
- package/dist/src/core/workspace.js +1 -0
- package/package.json +1 -1
- package/skills/zenrows/SKILL.md +7 -0
package/README.md
CHANGED
|
@@ -83,6 +83,18 @@ unclaimed Zenrows Free plan account**, stores the key in `.zenrows/secrets.json`
|
|
|
83
83
|
(0600, gitignored), and records the account in `.zenrows/account.json`
|
|
84
84
|
(no secret — just the accountId, Free-period info, and a claim link).
|
|
85
85
|
|
|
86
|
+
Zenrows may first ask the agent one short question before it creates the
|
|
87
|
+
account. The command then fails with `SIGNUP_CHALLENGE` and prints the
|
|
88
|
+
question; answer it and re-run the original command:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
zenrows signup --answer "<your answer>"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
A wrong or late answer gets a new question. Too many questions or wrong
|
|
95
|
+
answers from one network pause signups there for an hour
|
|
96
|
+
(`SIGNUP_CHALLENGE_LOCKED`).
|
|
97
|
+
|
|
86
98
|
When you (or your agent) want to keep the usage and history, **claim** the
|
|
87
99
|
account — this confirms an email (Google, GitHub or a 6-digit code); the same
|
|
88
100
|
key keeps working:
|
|
@@ -96,10 +108,11 @@ zenrows usage # plan usage, credits & concurrency (does not count again
|
|
|
96
108
|
Opt out of auto-provisioning at any time:
|
|
97
109
|
|
|
98
110
|
```bash
|
|
99
|
-
zenrows fetch <url> --no-signup
|
|
100
|
-
zenrows signup --agent
|
|
101
|
-
zenrows
|
|
102
|
-
zenrows login --
|
|
111
|
+
zenrows fetch <url> --no-signup # do not auto-create an account for this call
|
|
112
|
+
zenrows signup --agent # explicitly provision a Free plan account, headless
|
|
113
|
+
zenrows signup --answer "<text>" # answer the signup question (SIGNUP_CHALLENGE)
|
|
114
|
+
zenrows login --api-key <key> # use an existing key (stored 0600, gitignored)
|
|
115
|
+
zenrows login --env # use the ZENROWS_API_KEY environment variable
|
|
103
116
|
```
|
|
104
117
|
|
|
105
118
|
Set `auto_signup: false` in `.zenrows/policy.json` to disable auto-provisioning
|
|
@@ -27,8 +27,10 @@ cannot handle. **Escalation only.**
|
|
|
27
27
|
6. Always `browser_close` when done
|
|
28
28
|
|
|
29
29
|
Other useful tools: `browser_evaluate`, cookies (`browser_get_cookies` /
|
|
30
|
-
`browser_set_cookies` / `browser_clear_cookies`),
|
|
31
|
-
|
|
30
|
+
`browser_set_cookies` / `browser_clear_cookies`), local storage
|
|
31
|
+
(`browser_get_local_storage` / `browser_set_local_storage` /
|
|
32
|
+
`browser_clear_local_storage`), tabs (`browser_new_tab` / `browser_switch_tab`),
|
|
33
|
+
`browser_generate_pdf`.
|
|
32
34
|
|
|
33
35
|
## Rules
|
|
34
36
|
|
|
@@ -20,7 +20,10 @@ You do **not** need an API key in the plugin package.
|
|
|
20
20
|
|
|
21
21
|
- **stdio** (`npx -y @zenrows/mcp`): on first cloud call the server can
|
|
22
22
|
auto-create a **free, unclaimed** Zenrows Free plan account and persist the
|
|
23
|
-
key locally.
|
|
23
|
+
key locally. Zenrows may first ask one short question (which word is a
|
|
24
|
+
bird, which word does not belong): the tool call
|
|
25
|
+
returns it; answer it yourself with the `account_signup_answer` tool, then
|
|
26
|
+
retry the call. When the Free plan nears its limit (or the user asks), surface
|
|
24
27
|
the **claim URL** so a human can claim the account (confirming an email
|
|
25
28
|
with Google, GitHub or a 6-digit code). Claim links expire after 30 days. The
|
|
26
29
|
same key keeps working after claim. Never handle payment or upgrades —
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* There is no documented programmatic signup API, so we never fake account
|
|
5
5
|
* creation: we explain what is needed, open the signup URL, and hand off to
|
|
6
6
|
* `login`.
|
|
7
|
+
*
|
|
8
|
+
* `--agent` provisions a Free plan account headless, and `--answer` answers the
|
|
9
|
+
* one short question signup may ask an agent first (SIGNUP_CHALLENGE).
|
|
7
10
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
11
|
+
import { answerSignupChallenge, readAccount, signupAgent } from "../../core/agent-account.js";
|
|
12
|
+
import { storeSignup } from "../../core/ensure-key.js";
|
|
13
|
+
import { ToolkitError } from "../../core/errors.js";
|
|
10
14
|
import { attributionEnabled } from "../../core/config.js";
|
|
11
15
|
import { log } from "../../core/logger.js";
|
|
12
16
|
import { openUrl, SIGNUP_URL, withParams } from "../../core/open-url.js";
|
|
@@ -15,9 +19,42 @@ import { parse } from "../command.js";
|
|
|
15
19
|
export const signup = {
|
|
16
20
|
name: "signup",
|
|
17
21
|
summary: "Create a Zenrows account / obtain an API key (opens the browser).",
|
|
18
|
-
usage:
|
|
22
|
+
usage: 'zenrows signup [--agent] [--no-open] [--answer "<text>"]',
|
|
23
|
+
help: [
|
|
24
|
+
"Without flags, opens the Zenrows signup page in the browser.",
|
|
25
|
+
"",
|
|
26
|
+
" --agent Create a Free plan account headless and store its key.",
|
|
27
|
+
" --no-open Print the signup URL instead of opening it.",
|
|
28
|
+
' --answer "<text>" Answer the question Zenrows asked before creating an account',
|
|
29
|
+
" (error SIGNUP_CHALLENGE). Answer it yourself, then re-run",
|
|
30
|
+
" your original command.",
|
|
31
|
+
].join("\n"),
|
|
19
32
|
async run(argv, ctx) {
|
|
20
|
-
const { values } = parse(argv, {
|
|
33
|
+
const { values } = parse(argv, {
|
|
34
|
+
"no-open": { type: "boolean" },
|
|
35
|
+
agent: { type: "boolean" },
|
|
36
|
+
answer: { type: "string" },
|
|
37
|
+
});
|
|
38
|
+
if (values.answer !== undefined) {
|
|
39
|
+
const answer = String(values.answer).trim();
|
|
40
|
+
if (!answer) {
|
|
41
|
+
throw new ToolkitError({
|
|
42
|
+
code: "INVALID_USAGE",
|
|
43
|
+
message: "--answer needs the answer text.",
|
|
44
|
+
likely_cause: "An empty value was passed to --answer.",
|
|
45
|
+
next_action: 'Run: zenrows signup --answer "<your answer>"',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const res = await answerSignupChallenge(answer);
|
|
49
|
+
const account = storeSignup(res);
|
|
50
|
+
log.success("Right answer. Created a Zenrows Free plan account and stored its API key.");
|
|
51
|
+
log.dim(`Claim it anytime (keeps your usage): ${account.claimUrl}`);
|
|
52
|
+
log.info("Now re-run your original command.");
|
|
53
|
+
if (ctx.json) {
|
|
54
|
+
log.out(JSON.stringify({ ok: true, accountId: res.accountId, claimUrl: res.claimUrl, next_action: "Re-run your original command." }, null, 2));
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
21
58
|
if (values.agent) {
|
|
22
59
|
const account = readAccount();
|
|
23
60
|
if (account) {
|
|
@@ -25,9 +62,7 @@ export const signup = {
|
|
|
25
62
|
return 0;
|
|
26
63
|
}
|
|
27
64
|
const res = await signupAgent();
|
|
28
|
-
|
|
29
|
-
const now = new Date().toISOString();
|
|
30
|
-
writeAccount({ accountId: res.accountId, unclaimed: true, claimUrl: res.claimUrl, createdAt: now, claimUrlIssuedAt: now });
|
|
65
|
+
storeSignup(res);
|
|
31
66
|
log.success("Created a Zenrows Free plan account.");
|
|
32
67
|
log.dim(`Claim it anytime: ${res.claimUrl}`);
|
|
33
68
|
if (ctx.json)
|
|
@@ -12,6 +12,7 @@ import { ToolkitError } from "./errors.js";
|
|
|
12
12
|
import { AGENT_SIGNUP_API_URL, WELL_KNOWN_PROTECTED_RESOURCE } from "./open-url.js";
|
|
13
13
|
import { detectClient } from "./provenance.js";
|
|
14
14
|
import { findWorkspace, readJson, workspacePaths, writePrivateJson } from "./workspace.js";
|
|
15
|
+
import { challengeError, challengeExpired, challengeLockedError, clearChallenge, isChallengeLocked, noChallengeError, parseChallenge, readChallenge, wasWrongAnswer, writeChallenge, } from "./signup-challenge.js";
|
|
15
16
|
/** Env var to override the signup endpoint (highest priority; for local testing). */
|
|
16
17
|
export const SIGNUP_URL_ENV = "ZENROWS_AGENT_SIGNUP_URL";
|
|
17
18
|
/** Env var to override the discovery base origin (for local testing). */
|
|
@@ -117,8 +118,25 @@ export function clearAccount(projectRoot) {
|
|
|
117
118
|
return true;
|
|
118
119
|
}
|
|
119
120
|
export async function signupAgent(opts = {}) {
|
|
120
|
-
const urls = opts.url ? [opts.url] : await signupCandidates(
|
|
121
|
-
|
|
121
|
+
const urls = opts.url ? [opts.url] : await signupCandidates(opts.projectRoot, { fetchImpl: opts.fetchImpl });
|
|
122
|
+
return postSignup(urls, undefined, opts);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Answer the pending signup question (see signup-challenge.ts). Posts it to
|
|
126
|
+
* the signup URL that asked it; a right answer creates the account like a
|
|
127
|
+
* plain signup does. A wrong answer stores and reports the new question.
|
|
128
|
+
*/
|
|
129
|
+
export async function answerSignupChallenge(answer, opts = {}) {
|
|
130
|
+
const pending = readChallenge(opts.projectRoot);
|
|
131
|
+
if (!pending)
|
|
132
|
+
throw noChallengeError(false);
|
|
133
|
+
if (challengeExpired(pending)) {
|
|
134
|
+
clearChallenge(opts.projectRoot);
|
|
135
|
+
throw noChallengeError(true);
|
|
136
|
+
}
|
|
137
|
+
return postSignup([pending.signupUrl], { challenge_id: pending.id, answer }, opts);
|
|
138
|
+
}
|
|
139
|
+
function signupHeaders(projectRoot) {
|
|
122
140
|
// Signup is the one functional request the CLI must make — no separate
|
|
123
141
|
// telemetry beacon exists. When attribution is enabled we ride anonymous
|
|
124
142
|
// provenance on it (a random id + coarse host/runtime brand). No PII, no URLs.
|
|
@@ -126,7 +144,7 @@ export async function signupAgent(opts = {}) {
|
|
|
126
144
|
const headers = {
|
|
127
145
|
"content-type": "application/json",
|
|
128
146
|
"User-Agent": CLI_USER_AGENT,
|
|
129
|
-
...agentClientHeader(),
|
|
147
|
+
...agentClientHeader({ projectRoot }),
|
|
130
148
|
};
|
|
131
149
|
if (attributionEnabled()) {
|
|
132
150
|
const p = detectClient();
|
|
@@ -138,14 +156,21 @@ export async function signupAgent(opts = {}) {
|
|
|
138
156
|
headers["X-ZR-Node"] = p.node;
|
|
139
157
|
headers["X-ZR-CI"] = p.ci ? "1" : "0";
|
|
140
158
|
}
|
|
159
|
+
return headers;
|
|
160
|
+
}
|
|
161
|
+
async function postSignup(urls, body, opts) {
|
|
162
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
163
|
+
const headers = signupHeaders(opts.projectRoot);
|
|
141
164
|
// Try each candidate in order; on any failure fall back to the next (the last
|
|
142
165
|
// is always the built-in default). This is the safety net: a wrong/blocked
|
|
143
166
|
// discovered endpoint can never leave provisioning worse off than no discovery.
|
|
167
|
+
// A signup question or the wrong-answer lock is a real answer from a working
|
|
168
|
+
// endpoint, so it ends the loop.
|
|
144
169
|
let lastErr;
|
|
145
170
|
for (const url of urls) {
|
|
146
171
|
let res;
|
|
147
172
|
try {
|
|
148
|
-
res = await doFetch(url, { method: "POST", headers });
|
|
173
|
+
res = await doFetch(url, { method: "POST", headers, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
149
174
|
}
|
|
150
175
|
catch (err) {
|
|
151
176
|
lastErr = new ToolkitError({
|
|
@@ -156,9 +181,24 @@ export async function signupAgent(opts = {}) {
|
|
|
156
181
|
});
|
|
157
182
|
continue;
|
|
158
183
|
}
|
|
159
|
-
if (res.status === 201)
|
|
184
|
+
if (res.status === 201) {
|
|
185
|
+
clearChallenge(opts.projectRoot);
|
|
160
186
|
return (await res.json());
|
|
161
|
-
|
|
187
|
+
}
|
|
188
|
+
const text = await res.text();
|
|
189
|
+
if (res.status === 428) {
|
|
190
|
+
const challenge = parseChallenge(text, url);
|
|
191
|
+
if (challenge) {
|
|
192
|
+
writeChallenge(challenge, opts.projectRoot);
|
|
193
|
+
throw challengeError(challenge, { wrong: body !== undefined || wasWrongAnswer(text) });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (res.status === 429 && isChallengeLocked(text)) {
|
|
197
|
+
clearChallenge(opts.projectRoot);
|
|
198
|
+
const ra = Number(res.headers.get("retry-after"));
|
|
199
|
+
throw challengeLockedError(Number.isFinite(ra) && ra > 0 ? ra : undefined);
|
|
200
|
+
}
|
|
201
|
+
lastErr = signupError(res.status, text);
|
|
162
202
|
}
|
|
163
203
|
throw (lastErr ??
|
|
164
204
|
new ToolkitError({
|
package/dist/src/core/config.js
CHANGED
|
@@ -14,7 +14,7 @@ export const CONFIG_VERSION = "0.1.0";
|
|
|
14
14
|
* entrypoint) so core modules — telemetry, signup provenance — can read it
|
|
15
15
|
* without an import cycle. `VERSION` in `cli/index.ts` re-exports this.
|
|
16
16
|
*/
|
|
17
|
-
export const CLI_VERSION = "1.
|
|
17
|
+
export const CLI_VERSION = "1.10.0"; // x-release-please-version
|
|
18
18
|
/** The User-Agent on every request to a Zenrows API. The gateway reads the version from it. */
|
|
19
19
|
export const CLI_USER_AGENT = `zenrows-cli/${CLI_VERSION}`;
|
|
20
20
|
/** Env var to override the Fetch and Extract API base (local/staging testing). */
|
|
@@ -7,8 +7,15 @@ export async function ensureApiKey(policy, opts = {}) {
|
|
|
7
7
|
if (!policy.auto_signup)
|
|
8
8
|
return requireApiKey(opts.projectRoot); // throws AUTH_MISSING
|
|
9
9
|
const doSignup = opts.signupImpl ?? signupAgent;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// May throw SIGNUP_CHALLENGE: the agent answers with `zenrows signup --answer`.
|
|
11
|
+
const res = await doSignup({ projectRoot: opts.projectRoot });
|
|
12
|
+
const account = storeSignup(res, opts.projectRoot);
|
|
13
|
+
opts.onProvision?.(account);
|
|
14
|
+
return res.apiKey;
|
|
15
|
+
}
|
|
16
|
+
/** Persist a fresh agent signup: the key (0600) and the account metadata. */
|
|
17
|
+
export function storeSignup(res, projectRoot) {
|
|
18
|
+
saveApiKey(res.apiKey, projectRoot);
|
|
12
19
|
const account = {
|
|
13
20
|
accountId: res.accountId,
|
|
14
21
|
unclaimed: true,
|
|
@@ -16,7 +23,6 @@ export async function ensureApiKey(policy, opts = {}) {
|
|
|
16
23
|
createdAt: new Date().toISOString(),
|
|
17
24
|
};
|
|
18
25
|
account.claimUrlIssuedAt = account.createdAt;
|
|
19
|
-
writeAccount(account,
|
|
20
|
-
|
|
21
|
-
return res.apiKey;
|
|
26
|
+
writeAccount(account, projectRoot);
|
|
27
|
+
return account;
|
|
22
28
|
}
|
package/dist/src/core/errors.js
CHANGED
|
@@ -16,6 +16,7 @@ export class ToolkitError extends Error {
|
|
|
16
16
|
job_id;
|
|
17
17
|
idempotency_key;
|
|
18
18
|
retry_after;
|
|
19
|
+
challenge;
|
|
19
20
|
constructor(shape) {
|
|
20
21
|
super(shape.message);
|
|
21
22
|
this.name = "ToolkitError";
|
|
@@ -28,6 +29,7 @@ export class ToolkitError extends Error {
|
|
|
28
29
|
this.job_id = shape.job_id;
|
|
29
30
|
this.idempotency_key = shape.idempotency_key;
|
|
30
31
|
this.retry_after = shape.retry_after;
|
|
32
|
+
this.challenge = shape.challenge;
|
|
31
33
|
}
|
|
32
34
|
toJSON() {
|
|
33
35
|
return {
|
|
@@ -41,6 +43,7 @@ export class ToolkitError extends Error {
|
|
|
41
43
|
...(this.job_id ? { job_id: this.job_id } : {}),
|
|
42
44
|
...(this.idempotency_key ? { idempotency_key: this.idempotency_key } : {}),
|
|
43
45
|
...(this.retry_after !== undefined ? { retry_after: this.retry_after } : {}),
|
|
46
|
+
...(this.challenge ? { challenge: this.challenge } : {}),
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signup challenge: before it creates an account, the signup endpoint may
|
|
3
|
+
* answer 428 with one short natural-language question for the agent running
|
|
4
|
+
* the CLI to answer itself. The CLI never tries to solve it.
|
|
5
|
+
*
|
|
6
|
+
* Each CLI run is its own process, so the pending question is kept in
|
|
7
|
+
* `.zenrows/signup-challenge.json` (no secret: the id, the question, when it
|
|
8
|
+
* expires, and the signup URL that issued it, which is where the answer goes).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { ToolkitError } from "./errors.js";
|
|
13
|
+
import { findWorkspace, readJson, workspacePaths, writeJson } from "./workspace.js";
|
|
14
|
+
/** The command an agent runs to answer the pending question. */
|
|
15
|
+
export const ANSWER_COMMAND = 'zenrows signup --answer "<your answer>"';
|
|
16
|
+
export function challengePath(projectRoot) {
|
|
17
|
+
const ws = projectRoot ? workspacePaths(projectRoot) : (findWorkspace() ?? workspacePaths());
|
|
18
|
+
return join(ws.dir, "signup-challenge.json");
|
|
19
|
+
}
|
|
20
|
+
export function readChallenge(projectRoot) {
|
|
21
|
+
try {
|
|
22
|
+
const c = readJson(challengePath(projectRoot));
|
|
23
|
+
if (!c || typeof c.id !== "string" || typeof c.prompt !== "string" || typeof c.signupUrl !== "string")
|
|
24
|
+
return null;
|
|
25
|
+
return c;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function writeChallenge(c, projectRoot) {
|
|
32
|
+
writeJson(challengePath(projectRoot), c);
|
|
33
|
+
}
|
|
34
|
+
export function clearChallenge(projectRoot) {
|
|
35
|
+
const file = challengePath(projectRoot);
|
|
36
|
+
if (!existsSync(file))
|
|
37
|
+
return;
|
|
38
|
+
try {
|
|
39
|
+
unlinkSync(file);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// best effort
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** True when the stored question can no longer be answered. */
|
|
46
|
+
export function challengeExpired(c, now = Date.now()) {
|
|
47
|
+
const t = Date.parse(c.expiresAt);
|
|
48
|
+
return Number.isFinite(t) && t <= now;
|
|
49
|
+
}
|
|
50
|
+
function parseBody(body) {
|
|
51
|
+
try {
|
|
52
|
+
const json = JSON.parse(body);
|
|
53
|
+
return json && typeof json === "object" ? json : null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse a 428 `challenge_required` body into a pending challenge, or null when
|
|
61
|
+
* the body is not one (then the caller reports a generic failure).
|
|
62
|
+
*/
|
|
63
|
+
export function parseChallenge(body, signupUrl, now = Date.now()) {
|
|
64
|
+
const json = parseBody(body);
|
|
65
|
+
if (!json || json.error !== "challenge_required")
|
|
66
|
+
return null;
|
|
67
|
+
const ch = json.challenge;
|
|
68
|
+
if (!ch || typeof ch.id !== "string" || !ch.id || typeof ch.prompt !== "string" || !ch.prompt.trim())
|
|
69
|
+
return null;
|
|
70
|
+
const ttl = typeof ch.expires_in === "number" && ch.expires_in > 0 ? ch.expires_in : 600;
|
|
71
|
+
return { id: ch.id, prompt: ch.prompt.trim(), expiresAt: new Date(now + ttl * 1000).toISOString(), signupUrl };
|
|
72
|
+
}
|
|
73
|
+
/** True when a 428 says the previous answer was wrong or came too late. */
|
|
74
|
+
export function wasWrongAnswer(body) {
|
|
75
|
+
const msg = parseBody(body)?.message;
|
|
76
|
+
return typeof msg === "string" && /^wrong or expired/i.test(msg.trim());
|
|
77
|
+
}
|
|
78
|
+
/** True when a 429 is the per-network lock after too many questions or wrong answers. */
|
|
79
|
+
export function isChallengeLocked(body) {
|
|
80
|
+
return parseBody(body)?.error === "challenge_locked";
|
|
81
|
+
}
|
|
82
|
+
/** The error an agent sees while a question is waiting for its answer. */
|
|
83
|
+
export function challengeError(c, opts = {}) {
|
|
84
|
+
const lead = opts.wrong ? "That answer was wrong or came too late, so here is a new question. " : "";
|
|
85
|
+
return new ToolkitError({
|
|
86
|
+
code: "SIGNUP_CHALLENGE",
|
|
87
|
+
message: `${lead}Zenrows needs one quick answer before it creates an account: ${c.prompt}`,
|
|
88
|
+
likely_cause: "No API key is configured, and new accounts created by agents answer one short question first.",
|
|
89
|
+
next_action: `Answer the question yourself, then run: ${ANSWER_COMMAND}. When it succeeds, re-run your original command.`,
|
|
90
|
+
suggested_commands: [ANSWER_COMMAND, "zenrows login --api-key <your-key>"],
|
|
91
|
+
challenge: { id: c.id, question: c.prompt, expires_at: c.expiresAt },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export function challengeLockedError(retryAfter) {
|
|
95
|
+
return new ToolkitError({
|
|
96
|
+
code: "SIGNUP_CHALLENGE_LOCKED",
|
|
97
|
+
message: "Too many signup questions or wrong answers from this network, so Zenrows paused account creation here.",
|
|
98
|
+
likely_cause: "Too many signup questions asked, or wrong answers given, from this network within an hour.",
|
|
99
|
+
next_action: "Try again in an hour by re-running your original command, which asks a new question. If you already have a Zenrows API key, use it now: zenrows login --api-key <key> (or set ZENROWS_API_KEY).",
|
|
100
|
+
suggested_commands: ["zenrows login --api-key <your-key>"],
|
|
101
|
+
...(retryAfter !== undefined ? { retry_after: retryAfter } : {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function noChallengeError(expired) {
|
|
105
|
+
return new ToolkitError({
|
|
106
|
+
code: "SIGNUP_CHALLENGE_MISSING",
|
|
107
|
+
message: expired ? "The signup question expired before it was answered." : "There is no signup question waiting for an answer.",
|
|
108
|
+
likely_cause: expired
|
|
109
|
+
? "Questions are valid for a few minutes."
|
|
110
|
+
: "No signup was attempted from this directory, or the question was already answered.",
|
|
111
|
+
next_action: "Re-run your original command (for example zenrows fetch <url>) to get a new question, then answer it with: " + ANSWER_COMMAND,
|
|
112
|
+
suggested_commands: ["zenrows fetch <url>"],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -70,6 +70,7 @@ export function ensureGitignore(paths) {
|
|
|
70
70
|
`${WORKSPACE_DIRNAME}/secrets.json`,
|
|
71
71
|
`${WORKSPACE_DIRNAME}/config.json`,
|
|
72
72
|
`${WORKSPACE_DIRNAME}/account.json`,
|
|
73
|
+
`${WORKSPACE_DIRNAME}/signup-challenge.json`,
|
|
73
74
|
`${WORKSPACE_DIRNAME}/runs/`,
|
|
74
75
|
`${WORKSPACE_DIRNAME}/traces/`,
|
|
75
76
|
`${WORKSPACE_DIRNAME}/logs/`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenrows/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Open-source CLI, MCP, skills, templates, workflows, recipes, and evals layer for giving AI agents reliable access to protected web data through Zenrows cloud infrastructure.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/skills/zenrows/SKILL.md
CHANGED
|
@@ -20,6 +20,13 @@ the toolkit automatically creates a **free, unclaimed Zenrows Free plan account*
|
|
|
20
20
|
key locally — no human step required. To opt out, pass `--no-signup` or set
|
|
21
21
|
`auto_signup: false` in `.zenrows/policy.json`, then use `zenrows login --api-key <key>`.
|
|
22
22
|
|
|
23
|
+
Before it creates the account, Zenrows may ask you one short question (error code
|
|
24
|
+
`SIGNUP_CHALLENGE`, with the question in the message and in `error.challenge.question`).
|
|
25
|
+
Answer it yourself, do not ask the human, then run `zenrows signup --answer "<your answer>"`
|
|
26
|
+
and re-run your original command. A wrong answer returns a new question the same way.
|
|
27
|
+
After too many questions or wrong answers from one network (`SIGNUP_CHALLENGE_LOCKED`), wait an hour or use
|
|
28
|
+
`zenrows login --api-key <key>`.
|
|
29
|
+
|
|
23
30
|
When the Free plan nears its limit (or the user asks), tell the human they can **claim** the
|
|
24
31
|
account to keep their usage and history: run `zenrows account claim` (or open the claim URL
|
|
25
32
|
shown on quota errors). It prints a valid link, requesting a fresh one when the stored link is
|