@trawlme/cli 1.16.0 → 1.18.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 +24 -5
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +52 -3
- package/dist/commands/login.js +29 -9
- package/dist/commands/scraps.js +225 -95
- package/dist/commands/telemetry.js +9 -5
- package/dist/commands/token.js +7 -21
- package/dist/index.d.ts +19 -1
- package/dist/index.js +113 -42
- package/dist/lib/api.d.ts +14 -1
- package/dist/lib/api.js +133 -19
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.js +24 -0
- package/dist/lib/errors.d.ts +41 -0
- package/dist/lib/errors.js +59 -0
- package/dist/lib/jwt.d.ts +8 -0
- package/dist/lib/jwt.js +22 -0
- package/dist/lib/posthog.d.ts +9 -0
- package/dist/lib/posthog.js +47 -3
- package/dist/lib/prompt.js +13 -2
- package/dist/lib/validate.d.ts +9 -0
- package/dist/lib/validate.js +24 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,27 +26,43 @@ Custom API URL: `trawl login --url https://self-hosted.example.com`
|
|
|
26
26
|
|
|
27
27
|
## Commands
|
|
28
28
|
|
|
29
|
+
All commands accept a global `--debug` flag to show full error stack traces on failure.
|
|
30
|
+
|
|
29
31
|
### Auth
|
|
30
32
|
|
|
31
33
|
```
|
|
32
34
|
trawl login [--url <url>] [--token <jwt>] [--email <email>] [--password <pass>]
|
|
33
35
|
trawl logout
|
|
36
|
+
trawl token Print the stored session JWT (for MCP Bearer auth)
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
### Scraps
|
|
37
40
|
|
|
38
41
|
```
|
|
39
|
-
trawl scraps list [--json] [--status <success|failure|never>]
|
|
42
|
+
trawl scraps list [--json] [--status <success|failure|never>] [--limit <n>] [--page <n>]
|
|
40
43
|
trawl scraps get <id> [--json]
|
|
41
|
-
trawl scraps create -t <title> [-u <url>] [-r <request>] [-
|
|
42
|
-
trawl scraps update <id> [-t title] [-u <url>] [-r request] [-
|
|
44
|
+
trawl scraps create -t <title> [-u <url>] [-r <request>] [-d <description>] [--tier <tier0|tier1|tier2|tier3|tier4>]
|
|
45
|
+
trawl scraps update <id> [-t <title>] [-u <url>] [-r <request>] [-d <description>] [--cron <expr>|--no-cron] [--alert <email>|--no-alert] [--autofix|--no-autofix] [-p <json>|--params-file <path>] [--tier <tier0|tier1|tier2|tier3|tier4>] [--force-tier <tier0|tier1|tier2|tier3|tier4>]
|
|
43
46
|
trawl scraps run <id> [--watch]
|
|
44
|
-
trawl scraps trigger <id> [--watch]
|
|
47
|
+
trawl scraps trigger <id> [--watch] [--wait]
|
|
45
48
|
trawl scraps watch <id>
|
|
46
|
-
trawl scraps data <id> [--json]
|
|
49
|
+
trawl scraps data <id> [--json] [--fresh] [--errors]
|
|
50
|
+
trawl scraps history <id> [--json] [-n <limit>]
|
|
51
|
+
trawl scraps run-info <hid> [--json]
|
|
52
|
+
trawl scraps doctor <id> [--json] [--autofix]
|
|
53
|
+
trawl scraps autofix <id> [--json]
|
|
54
|
+
trawl scraps snapshot <id> [--error] [-o <file>]
|
|
55
|
+
trawl scraps banner <id> -f <file>
|
|
47
56
|
trawl scraps rm <id> [--force]
|
|
48
57
|
```
|
|
49
58
|
|
|
59
|
+
- `--tier` forces a proxy tier; `--force-tier` raises the proxy-tier ceiling past the auto-cap (history-gated: may be refused or cost more).
|
|
60
|
+
- `scraps doctor` diagnoses the last run (error, failed selector, block status, page state, autofix outcome); `--autofix` includes the full autofix diff/dry-run/knowledge.
|
|
61
|
+
- `scraps autofix` shows the last auto-fix attempt on its own (decision, diff, dry-run, knowledge).
|
|
62
|
+
- `scraps snapshot --error` fetches the error-path snapshot instead of the normal one; `-o <file>` writes to a file instead of stdout.
|
|
63
|
+
- `scraps history` lists past runs (newest first); `scraps run-info <hid>` shows details of a single run from that history.
|
|
64
|
+
- `scraps data` returns the last persisted run payload (no execute quota); `--fresh` runs the scrap live instead (consumes execute quota); `--errors` shows the last run's error detail.
|
|
65
|
+
|
|
50
66
|
### Scrap accounts
|
|
51
67
|
|
|
52
68
|
```
|
|
@@ -54,8 +70,11 @@ trawl scraps account set <id> [-u <username>] [-p <password>]
|
|
|
54
70
|
trawl scraps account delete <id> [--force]
|
|
55
71
|
trawl scraps account clear-session <id>
|
|
56
72
|
trawl scraps account status <id> [--json]
|
|
73
|
+
trawl scraps account session set <id> -c <file>
|
|
57
74
|
```
|
|
58
75
|
|
|
76
|
+
`account session set` uploads a Puppeteer cookie JSON array to bootstrap a logged-in session without storing credentials (BYO-cookies).
|
|
77
|
+
|
|
59
78
|
### Claude Code skills
|
|
60
79
|
|
|
61
80
|
The CLI bundles a Claude Code skill that teaches Claude how to use `trawl`. Once installed, Claude can manage scraps for you via prompts.
|
|
@@ -25,6 +25,7 @@ export interface Run {
|
|
|
25
25
|
selectors?: Record<string, number>;
|
|
26
26
|
} | null;
|
|
27
27
|
blocked?: boolean;
|
|
28
|
+
blockType?: string | null;
|
|
28
29
|
proxyTier?: string | null;
|
|
29
30
|
regressionDetected?: boolean;
|
|
30
31
|
baselineLength?: number | null;
|
|
@@ -33,6 +34,14 @@ export interface Run {
|
|
|
33
34
|
time?: number | null;
|
|
34
35
|
triggeredBy?: string | null;
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a known anti-bot vendor (or auth) name from a worker `blockType`
|
|
39
|
+
* string. Returns null when the run isn't blocked, or when it's blocked by
|
|
40
|
+
* something other than a known vendor (e.g. `proxy-domain-gate`,
|
|
41
|
+
* `rate_limited_per_host`) — those stay on the genuine-error path since we
|
|
42
|
+
* can't honestly attribute them to a specific "no reliable bypass" wall.
|
|
43
|
+
*/
|
|
44
|
+
export declare function detectWallVendor(run: Pick<Run, 'blocked' | 'blockType'>): string | null;
|
|
36
45
|
/**
|
|
37
46
|
* Autofix activity metadata — from the persisted ai_fix_end activity.
|
|
38
47
|
* aiUsage (cost) is stripped server-side; all diagnostics are kept.
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
import { api } from '../lib/api.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
/**
|
|
4
|
+
* Known anti-bot vendors (+ auth) that the worker's `blockType` field may name.
|
|
5
|
+
* Ordered by first-match; `blockType` is a freeform worker string, not an enum
|
|
6
|
+
* (see the `Run.blockType` doc comment), so this is a best-effort substring
|
|
7
|
+
* match against the real field — never an invented/mocked value.
|
|
8
|
+
*
|
|
9
|
+
* `datadome`/`perimeterx`/`akamai`/`cloudflare` are literal substrings the
|
|
10
|
+
* current worker emits (detect.js). `kasada` and `auth` are forward-compatible:
|
|
11
|
+
* the issue (#62 / wall-registry WS-7) names them as genuine walls, but the
|
|
12
|
+
* current worker vocabulary does not yet emit them — these branches light up
|
|
13
|
+
* automatically if/when a future worker classifier does, without a CLI change.
|
|
14
|
+
*
|
|
15
|
+
* The `auth` pattern is delimiter-anchored (`^`/`$`/`-_:`) rather than a bare
|
|
16
|
+
* substring so it matches worker-style tokens (`auth`, `auth-wall`, `auth_wall`,
|
|
17
|
+
* `login:auth`) without false-matching `oauth`/`authorization`/`author`. It uses
|
|
18
|
+
* `[-_:]` (not `\b`) because `_` is a JS `\w` char, so `\bauth\b` would miss the
|
|
19
|
+
* underscore-delimited `auth_wall` form the worker's `rate_limited_per_host`-style
|
|
20
|
+
* naming favors.
|
|
21
|
+
*/
|
|
22
|
+
const WALL_VENDOR_PATTERNS = [
|
|
23
|
+
[/datadome/i, 'DataDome'],
|
|
24
|
+
[/kasada/i, 'Kasada'],
|
|
25
|
+
[/perimeterx/i, 'PerimeterX'],
|
|
26
|
+
[/akamai/i, 'Akamai'],
|
|
27
|
+
[/cloudflare/i, 'Cloudflare'],
|
|
28
|
+
[/(?:^|[-_:])auth(?:$|[-_:])/i, 'auth'],
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a known anti-bot vendor (or auth) name from a worker `blockType`
|
|
32
|
+
* string. Returns null when the run isn't blocked, or when it's blocked by
|
|
33
|
+
* something other than a known vendor (e.g. `proxy-domain-gate`,
|
|
34
|
+
* `rate_limited_per_host`) — those stay on the genuine-error path since we
|
|
35
|
+
* can't honestly attribute them to a specific "no reliable bypass" wall.
|
|
36
|
+
*/
|
|
37
|
+
export function detectWallVendor(run) {
|
|
38
|
+
if (run.blocked !== true || !run.blockType)
|
|
39
|
+
return null;
|
|
40
|
+
for (const [pattern, label] of WALL_VENDOR_PATTERNS) {
|
|
41
|
+
if (pattern.test(run.blockType))
|
|
42
|
+
return label;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
3
46
|
const TIER_LABELS = {
|
|
4
47
|
tier0: 'Tier 0',
|
|
5
48
|
tier1: 'Tier 1',
|
|
@@ -9,7 +52,7 @@ const TIER_LABELS = {
|
|
|
9
52
|
};
|
|
10
53
|
const RUN_ALLOWLIST = [
|
|
11
54
|
'_id', 'status', 'statusDetail', 'length', 'errorMessage', 'errorSnapshot',
|
|
12
|
-
'emptyContext', 'blocked', 'proxyTier', 'regressionDetected', 'baselineLength',
|
|
55
|
+
'emptyContext', 'blocked', 'blockType', 'proxyTier', 'regressionDetected', 'baselineLength',
|
|
13
56
|
'fixVersionId', 'createdAt', 'time', 'triggeredBy',
|
|
14
57
|
];
|
|
15
58
|
const FIX_ALLOWLIST = [
|
|
@@ -49,9 +92,15 @@ export function formatDoctor(scrapTitle, run, fix = null, scrapId) {
|
|
|
49
92
|
: chalk.red('● failed');
|
|
50
93
|
lines.push(`${chalk.bold(scrapTitle)} ${badge}${run.statusDetail ? ` (${run.statusDetail})` : ''}`);
|
|
51
94
|
lines.push(chalk.dim(` Run ID: ${run._id}`));
|
|
52
|
-
// Error message
|
|
95
|
+
// Error message — an honest accept-wall string for known-walled scraps
|
|
96
|
+
// (DataDome/Kasada/PerimeterX/Akamai/auth terminal verdict from the worker),
|
|
97
|
+
// otherwise the real error (genuine transient failure).
|
|
98
|
+
const wallVendor = detectWallVendor(run);
|
|
53
99
|
const errMsg = run.errorMessage ?? run.errorSnapshot?.errorMessage;
|
|
54
|
-
if (
|
|
100
|
+
if (wallVendor) {
|
|
101
|
+
lines.push(chalk.dim(' Error: ') + chalk.red(`walled: ${wallVendor} — no reliable bypass`));
|
|
102
|
+
}
|
|
103
|
+
else if (errMsg) {
|
|
55
104
|
lines.push(chalk.dim(' Error: ') + chalk.red(errMsg));
|
|
56
105
|
}
|
|
57
106
|
// Failed selector
|
package/dist/commands/login.js
CHANGED
|
@@ -2,14 +2,23 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import config, { getApiUrl } from '../lib/config.js';
|
|
4
4
|
import { api } from '../lib/api.js';
|
|
5
|
-
import {
|
|
5
|
+
import { requireFreshJwt, requireUrl } from '../lib/validate.js';
|
|
6
6
|
import { promptPassword } from '../lib/prompt.js';
|
|
7
7
|
async function promptEmail() {
|
|
8
8
|
const { createInterface } = await import('readline');
|
|
9
9
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
10
10
|
try {
|
|
11
|
-
return await new Promise((resolve) => {
|
|
12
|
-
|
|
11
|
+
return await new Promise((resolve, reject) => {
|
|
12
|
+
let answered = false;
|
|
13
|
+
rl.question('Email: ', (answer) => {
|
|
14
|
+
answered = true;
|
|
15
|
+
resolve(answer.trim());
|
|
16
|
+
});
|
|
17
|
+
rl.on('close', () => {
|
|
18
|
+
if (!answered) {
|
|
19
|
+
reject(new Error('No input received — pass -e/-p or set TRAWL_TOKEN for non-interactive use'));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
13
22
|
});
|
|
14
23
|
}
|
|
15
24
|
finally {
|
|
@@ -23,12 +32,21 @@ export const login = new Command('login')
|
|
|
23
32
|
.option('-e, --email <email>', 'Email address')
|
|
24
33
|
.option('-p, --password <password>', 'Password (CI only — visible in process list and shell history)')
|
|
25
34
|
.action(async (opts) => {
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
// Validate --url but do NOT persist it yet. A failed signin must not
|
|
36
|
+
// brick the config by pointing it at an unreachable/wrong host while the
|
|
37
|
+
// OLD token stays stored (and would then be sent to that new host on the
|
|
38
|
+
// next command). Target this run via `pendingUrl` and persist only once
|
|
39
|
+
// a token has actually been obtained. (#68)
|
|
40
|
+
const pendingUrl = opts.url ? requireUrl(opts.url, '--url') : undefined;
|
|
41
|
+
const persistUrlIfPending = () => {
|
|
42
|
+
if (pendingUrl)
|
|
43
|
+
config.set('apiUrl', pendingUrl);
|
|
44
|
+
};
|
|
28
45
|
// Check environment variable override first
|
|
29
46
|
const envToken = process.env['TRAWL_TOKEN'];
|
|
30
47
|
if (envToken) {
|
|
31
|
-
config.set('token',
|
|
48
|
+
config.set('token', requireFreshJwt(envToken, 'TRAWL_TOKEN'));
|
|
49
|
+
persistUrlIfPending();
|
|
32
50
|
console.log(chalk.green('✓ Logged in'));
|
|
33
51
|
console.log(chalk.dim(` API: ${getApiUrl()}`));
|
|
34
52
|
console.log(chalk.dim(` Config: ${config.path}`));
|
|
@@ -36,7 +54,8 @@ export const login = new Command('login')
|
|
|
36
54
|
}
|
|
37
55
|
// --token flag: direct JWT (CI / retrocompat)
|
|
38
56
|
if (opts.token) {
|
|
39
|
-
config.set('token',
|
|
57
|
+
config.set('token', requireFreshJwt(opts.token, '--token'));
|
|
58
|
+
persistUrlIfPending();
|
|
40
59
|
console.log(chalk.green('✓ Logged in'));
|
|
41
60
|
console.log(chalk.dim(` API: ${getApiUrl()}`));
|
|
42
61
|
console.log(chalk.dim(` Config: ${config.path}`));
|
|
@@ -48,7 +67,7 @@ export const login = new Command('login')
|
|
|
48
67
|
}
|
|
49
68
|
const email = opts.email ?? (await promptEmail());
|
|
50
69
|
const password = opts.password ?? (await promptPassword('Password: '));
|
|
51
|
-
const { data, headers } = await api.publicPost('/api/auth/signin', { email, password });
|
|
70
|
+
const { data, headers } = await api.publicPost('/api/auth/signin', { email, password }, pendingUrl);
|
|
52
71
|
// Try token from response body first, then fall back to Set-Cookie header
|
|
53
72
|
let raw = typeof data === 'string' ? data : data.token;
|
|
54
73
|
if (!raw) {
|
|
@@ -60,8 +79,9 @@ export const login = new Command('login')
|
|
|
60
79
|
if (!raw || typeof raw !== 'string') {
|
|
61
80
|
throw new Error(`Invalid response from API: no token received. Got: ${JSON.stringify(data)}`);
|
|
62
81
|
}
|
|
63
|
-
const token =
|
|
82
|
+
const token = requireFreshJwt(raw, 'token');
|
|
64
83
|
config.set('token', token);
|
|
84
|
+
persistUrlIfPending();
|
|
65
85
|
console.log(chalk.green(`✓ Logged in as ${email}`));
|
|
66
86
|
console.log(chalk.dim(` API: ${getApiUrl()}`));
|
|
67
87
|
console.log(chalk.dim(` Config: ${config.path}`));
|