@trawlme/cli 3.5.0 → 3.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/README.md +6 -2
- package/dist/commands/create.js +1 -1
- package/dist/commands/ping.js +8 -1
- package/dist/commands/scraps.js +12 -1
- package/dist/index.js +23 -1
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.js +42 -0
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.js +1 -0
- package/dist/lib/tips.d.ts +30 -0
- package/dist/lib/tips.js +69 -0
- package/docs/agent-quickstart.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Command-line client for [Trawl](https://trawl.me) — manage your scraps from the terminal.
|
|
8
8
|
|
|
9
|
-
>
|
|
9
|
+
> 📌 **Versioned with semantic-release** — breaking CLI-surface changes always ship as a new major. Pin a major (`@trawlme/cli@^3`) if you depend on the CLI in CI.
|
|
10
10
|
|
|
11
11
|
> 🤖 **Driving this CLI from an AI agent?** See [docs/agent-quickstart.md](docs/agent-quickstart.md) for the minimal surface (`create`, `--json`, `TRAWL_TOKEN`, exit codes) — this README is the full human/CI guide.
|
|
12
12
|
|
|
@@ -48,7 +48,7 @@ All commands accept a global `--debug` flag to show full error stack traces on f
|
|
|
48
48
|
```
|
|
49
49
|
trawl create <url> --prompt <goal> [--no-autofix] [--json] # url also accepted as --url <url> (#116)
|
|
50
50
|
Create a persistent, self-healing scrap from a URL + a goal (AI-generated)
|
|
51
|
-
trawl run <id> [--watch] [--json] Run a scrap
|
|
51
|
+
trawl run <id> [--watch] [--json] Run a scrap (blocks until finished)
|
|
52
52
|
trawl list|ls [--json] [--status <success|failure|never|running|regression>] [--limit <n>] [--page <n>]
|
|
53
53
|
trawl get <id> [--json] Get scrap details
|
|
54
54
|
trawl data <id> [--json] [--fresh] [--errors] Get scrap data (last persisted run — read-only, no quota; --fresh to launch one)
|
|
@@ -59,6 +59,8 @@ trawl whoami [--json] Show the authenticated user's ide
|
|
|
59
59
|
trawl ping [--json] Health/version handshake against the Trawl API
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
(`trawl create` AI-generates and persists a new scrap from a URL + a goal — distinct from `trawl scraps create`, which is raw manual scrap entry.)
|
|
63
|
+
|
|
62
64
|
> **No breaking change:** every verb above is also still reachable under its pre-reorg path, `trawl scraps <verb>` (e.g. `trawl scraps list`, `trawl scraps run <id>`) — kept as a hidden alias so scripts written before the surface reorg keep working. `trawl --help` only shows the top-level form above; `trawl scraps --help` only shows the remaining scrap-management commands below.
|
|
63
65
|
|
|
64
66
|
`create`/`whoami`/`ping` are fully non-interactive — all three read auth only from `TRAWL_TOKEN`/the stored login token, never prompt. `trawl create` runs the AI wizard server-side (`POST /api/ai/wizard`): generate scrap code from `--prompt` via LLM, persist the scrap, trigger its FIRST run, and auto-fix on failure (default on — `--no-autofix` disables it, sending `autoFix:false`). On a successful first run (human mode) it prints a small **data sample** (item count + first-item fields + one truncated value) as proof of value — best-effort, silent if the sample can't be fetched — and points `Next step` at `trawl data <id>` (the data), with `trawl get <id>` as the secondary detail view. `--json` skips the sample fetch and prints the raw wizard payload verbatim. `success` is an honest outcome of that first run, not "did the HTTP call succeed" — a failed first run is still a 200 response (the scrap was still created; auto-fix, when enabled, retries in the background), and the CLI exits 1 in that case (both human and `--json` modes) even though `--json` always prints the raw payload verbatim. The call legitimately takes 30–250s+ server-side (AI generation + a real run), same long-run timeout as `run`/`data --fresh`/`trigger --wait` below. `trawl whoami`/`trawl ping` mirror the MCP `trawl_whoami`/`trawl_health_ping` tools as closely as the REST surface allows (`GET /api/users/me` / `GET /api/health`) — `ping`'s `--json` payload is admin-enriched (version/uptime/db) and just `{"status":"ok"}` for anyone else.
|
|
@@ -243,6 +245,8 @@ This project uses `legacy-peer-deps` (see `.npmrc`) due to a transitive peer-dep
|
|
|
243
245
|
|
|
244
246
|
Issues and PRs are welcome — please open an issue first for non-trivial changes so we can align on direction.
|
|
245
247
|
|
|
248
|
+
Releases: a merge to `main` publishes to the npm **`next`** dist-tag (candidate channel — try it with `npm i -g @trawlme/cli@next`). The **`latest`** tag is promoted manually after QA, so a merge never ships to default installs directly.
|
|
249
|
+
|
|
246
250
|
## License
|
|
247
251
|
|
|
248
252
|
MIT — see [LICENSE](./LICENSE).
|
package/dist/commands/create.js
CHANGED
|
@@ -88,7 +88,7 @@ async function printDataSample(historyId) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
export const create = new Command('create')
|
|
91
|
-
.description('Create a persistent, self-healing scrap from a URL + a goal (AI-generated)')
|
|
91
|
+
.description('Create a persistent, self-healing scrap from a URL + a goal (AI-generated) — distinct from `trawl scraps create`, which is raw manual entry')
|
|
92
92
|
.argument('[url]', 'Target public URL (http/https)')
|
|
93
93
|
.requiredOption('--prompt <goal>', 'What to extract/scrape, in plain language (required)')
|
|
94
94
|
.option('--url <url>', 'Target public URL — alias of the positional argument')
|
package/dist/commands/ping.js
CHANGED
|
@@ -6,7 +6,14 @@ export const ping = new Command('ping')
|
|
|
6
6
|
.description('Health/version handshake against the Trawl API')
|
|
7
7
|
.option('--json', 'Output as JSON')
|
|
8
8
|
.action(async (opts) => {
|
|
9
|
-
|
|
9
|
+
// #148 — the server route is optionalAuth (public, enriched for an admin
|
|
10
|
+
// JWT — see the PingResponse doc above); `api.publicGet` mirrors that:
|
|
11
|
+
// it attaches a stored/env token when one happens to be available but
|
|
12
|
+
// never REQUIRES one, so `ping` works as a zero-config sanity check even
|
|
13
|
+
// with no `trawl login` ever run. `api.get` (the authenticated path)
|
|
14
|
+
// would throw notLoggedInError() locally before this ever reached the
|
|
15
|
+
// network, defeating the whole point of a pre-login handshake.
|
|
16
|
+
const data = await api.publicGet('/api/health');
|
|
10
17
|
// #106 review (kimi) — honest exit code: a soft-degraded 200 (status !== 'ok')
|
|
11
18
|
// must exit non-zero so `trawl ping || handle_degraded` doesn't treat a
|
|
12
19
|
// degraded API as healthy. Applies in BOTH --json and human modes, alongside
|
package/dist/commands/scraps.js
CHANGED
|
@@ -9,6 +9,7 @@ import { classifyError, reportError, UsageError, RefusalError } from '../lib/err
|
|
|
9
9
|
import { confirmDestructive, isInteractive } from '../lib/confirm.js';
|
|
10
10
|
import { formatDoctor, formatAutofix, fetchRunAndFix, pickRun, pickFix } from './doctor.js';
|
|
11
11
|
import { renderPinch, pinchEnabled } from '../lib/pinch.js';
|
|
12
|
+
import { maybeShowReferralTip } from '../lib/tips.js';
|
|
12
13
|
/**
|
|
13
14
|
* Print a usage/validation error consistently: human text to stderr, or a
|
|
14
15
|
* machine envelope on stdout under --json (never both — reportError is the
|
|
@@ -710,7 +711,7 @@ scraps
|
|
|
710
711
|
export function attachRunCommand(parent, attachOpts = {}) {
|
|
711
712
|
return parent
|
|
712
713
|
.command('run <id>', attachOpts)
|
|
713
|
-
.description('Run a scrap')
|
|
714
|
+
.description('Run a scrap (blocks until finished)')
|
|
714
715
|
.option('-w, --watch', 'Show progress after launching (polls — see `trawl trigger --watch`, #91)')
|
|
715
716
|
.option('--json', 'Output the raw launch payload as JSON')
|
|
716
717
|
.action(async (id, opts) => {
|
|
@@ -738,6 +739,16 @@ export function attachRunCommand(parent, attachOpts = {}) {
|
|
|
738
739
|
// of what the watched run actually did.
|
|
739
740
|
await pollRunProgress(id, beforeRun, { json: opts.json });
|
|
740
741
|
}
|
|
742
|
+
// #151 — throttled post-run referral tip. Reached only when the
|
|
743
|
+
// launch call above didn't throw (a hard failure rejects it, unwinding
|
|
744
|
+
// straight to index.ts's central catch before this line ever runs)
|
|
745
|
+
// — and, when --watch polled to a terminal state, only when
|
|
746
|
+
// pollRunProgress didn't flag a genuine run failure/timeout/poll-error
|
|
747
|
+
// via a non-zero process.exitCode (#107 review F1). See lib/tips.ts
|
|
748
|
+
// for the throttle/TTY/json/opt-out gating itself.
|
|
749
|
+
const runFailed = typeof process.exitCode === 'number' && process.exitCode !== 0;
|
|
750
|
+
if (!runFailed)
|
|
751
|
+
maybeShowReferralTip({ json: opts.json });
|
|
741
752
|
});
|
|
742
753
|
}
|
|
743
754
|
attachRunCommand(scraps, { hidden: true });
|
package/dist/index.js
CHANGED
|
@@ -293,7 +293,29 @@ export async function runCli(argv = process.argv) {
|
|
|
293
293
|
}
|
|
294
294
|
});
|
|
295
295
|
try {
|
|
296
|
-
|
|
296
|
+
// #148 — a bare `trawl` (no args at all) reads as "let me look around",
|
|
297
|
+
// not a failure: it should behave exactly like `trawl --help` (same
|
|
298
|
+
// text, stdout, exit 0). Left to commander's own default, a program with
|
|
299
|
+
// subcommands and no root action handler treats zero args as "probably
|
|
300
|
+
// missing subcommand" and calls `this.help({ error: true })` internally
|
|
301
|
+
// (see node_modules/commander/lib/command.js `_parseCommand`) — which
|
|
302
|
+
// writes the SAME help text to stderr and throws a CommanderError with
|
|
303
|
+
// exitCode 1 (caught below, bucketed into HELP_OR_VERSION_CODES since its
|
|
304
|
+
// code is 'commander.help', so only the exit code carries the mistake
|
|
305
|
+
// through — text already went to the wrong stream by then). Intercepting
|
|
306
|
+
// here, before parseAsync ever runs, avoids fighting that internal
|
|
307
|
+
// decision entirely and can't affect a genuine unknown top-level command
|
|
308
|
+
// (`trawl frobnicate`) — that path only runs when `isBareInvocation` is
|
|
309
|
+
// false, so it still reaches `this.unknownCommand()` unchanged (stderr,
|
|
310
|
+
// exit 2, code 'commander.unknownCommand' — never in
|
|
311
|
+
// HELP_OR_VERSION_CODES).
|
|
312
|
+
if (isBareInvocation(argv)) {
|
|
313
|
+
program.outputHelp();
|
|
314
|
+
process.exitCode = 0;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
await program.parseAsync(argv);
|
|
318
|
+
}
|
|
297
319
|
}
|
|
298
320
|
catch (err) {
|
|
299
321
|
const { debug } = program.opts();
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ export interface RequestOptions {
|
|
|
60
60
|
}
|
|
61
61
|
export declare const api: {
|
|
62
62
|
get: <T>(path: string, opts?: RequestOptions) => Promise<T>;
|
|
63
|
+
publicGet: <T>(path: string, opts?: RequestOptions) => Promise<T>;
|
|
63
64
|
getText: (path: string, opts?: RequestOptions) => Promise<string>;
|
|
64
65
|
post: <T>(path: string, body?: unknown, opts?: RequestOptions) => Promise<T>;
|
|
65
66
|
put: <T>(path: string, body?: unknown, opts?: RequestOptions) => Promise<T>;
|
package/dist/lib/api.js
CHANGED
|
@@ -314,6 +314,47 @@ async function publicPost(path, body, baseUrlOverride, reqOpts = {}) {
|
|
|
314
314
|
throw new Error('Invalid JSON in server response');
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Unauthenticated GET — for endpoints the server itself treats as public/
|
|
319
|
+
* optional-auth (e.g. `GET /api/health`, home.route.js's `optionalAuth`
|
|
320
|
+
* middleware: enriches the response for an admin JWT, passes through
|
|
321
|
+
* otherwise). Unlike `request()`, this NEVER throws `notLoggedInError()` for
|
|
322
|
+
* a missing token — that guard exists for endpoints that genuinely require
|
|
323
|
+
* auth, and applying it here defeated the whole point of a pre-login
|
|
324
|
+
* sanity-check (#148). The token is still attached as a best-effort Cookie
|
|
325
|
+
* when one happens to be stored/env-set, so an already-authenticated caller
|
|
326
|
+
* still gets the admin-enriched payload — it's just never REQUIRED.
|
|
327
|
+
* `throwIfError(res, true)` mirrors `publicPost`'s "public endpoint" 401
|
|
328
|
+
* wording, even though this route's optionalAuth middleware never actually
|
|
329
|
+
* rejects a request for lacking/invalid credentials.
|
|
330
|
+
*/
|
|
331
|
+
async function publicGet(path, reqOpts = {}) {
|
|
332
|
+
const token = getToken();
|
|
333
|
+
const url = `${getApiUrl()}${path}`;
|
|
334
|
+
const timeoutMs = getTimeoutMs(reqOpts.timeoutMs);
|
|
335
|
+
const res = await safeFetch(url, {
|
|
336
|
+
headers: {
|
|
337
|
+
'User-Agent': USER_AGENT,
|
|
338
|
+
...(token ? { Cookie: `TOKEN=${token}` } : {}),
|
|
339
|
+
},
|
|
340
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
341
|
+
}, timeoutMs);
|
|
342
|
+
await throwIfError(res, true);
|
|
343
|
+
const text = await res.text();
|
|
344
|
+
try {
|
|
345
|
+
if (!text)
|
|
346
|
+
return {};
|
|
347
|
+
const parsed = JSON.parse(text);
|
|
348
|
+
// Unwrap API envelope { type, message, data: T }
|
|
349
|
+
if (parsed !== null && typeof parsed === 'object' && 'data' in parsed) {
|
|
350
|
+
return parsed.data;
|
|
351
|
+
}
|
|
352
|
+
return parsed;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
throw new Error('Invalid JSON in server response');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
317
358
|
async function getText(path, reqOpts = {}) {
|
|
318
359
|
const token = getToken();
|
|
319
360
|
if (!token)
|
|
@@ -332,6 +373,7 @@ async function getText(path, reqOpts = {}) {
|
|
|
332
373
|
}
|
|
333
374
|
export const api = {
|
|
334
375
|
get: (path, opts) => request(path, {}, opts),
|
|
376
|
+
publicGet: (path, opts) => publicGet(path, opts),
|
|
335
377
|
getText: (path, opts) => getText(path, opts),
|
|
336
378
|
post: (path, body, opts) => request(path, {
|
|
337
379
|
method: 'POST',
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ interface TrawlConfig {
|
|
|
9
9
|
* only an explicit `false` disables it. No default entry needed since it's
|
|
10
10
|
* optional. (#129) */
|
|
11
11
|
updateNotifier?: boolean;
|
|
12
|
+
/** Opt-out switch for the throttled post-run referral tip (lib/tips.ts).
|
|
13
|
+
* Same optional/absent-means-enabled shape as `updateNotifier` above. */
|
|
14
|
+
tips?: boolean;
|
|
15
|
+
referralTipShownAt: number;
|
|
12
16
|
}
|
|
13
17
|
declare const config: Conf<TrawlConfig>;
|
|
14
18
|
/**
|
package/dist/lib/config.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure gate: true when the tip should print. Every external signal (json
|
|
3
|
+
* flag, TTY-ness, opt-out, persisted timestamp, current time) is a
|
|
4
|
+
* parameter rather than read internally, so this is trivially testable
|
|
5
|
+
* without mocking process.env/stdout/Date/config.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isReferralTipDue(opts: {
|
|
8
|
+
json?: boolean;
|
|
9
|
+
isTTY: boolean;
|
|
10
|
+
optedOut: boolean;
|
|
11
|
+
lastShownAt: number;
|
|
12
|
+
now: number;
|
|
13
|
+
}): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Best-effort: print one subtle line inviting the user to refer a builder,
|
|
16
|
+
* throttled to once per 7 days via a timestamp persisted in the CLI's
|
|
17
|
+
* existing Conf-backed config store (lib/config.ts — same store as
|
|
18
|
+
* apiUrl/token/telemetry/updateNotifier).
|
|
19
|
+
*
|
|
20
|
+
* This only gates on throttle/TTY/`--json`/opt-out — it does NOT know
|
|
21
|
+
* whether the underlying command actually succeeded. Callers must only
|
|
22
|
+
* invoke it on a genuine success (see commands/scraps.ts `run <id>`'s
|
|
23
|
+
* action, which also checks pollRunProgress's exit code under `--watch`
|
|
24
|
+
* before calling this). Any state read/write failure (corrupt config file,
|
|
25
|
+
* disk error, …) is swallowed silently — a broken tip must never break a
|
|
26
|
+
* run.
|
|
27
|
+
*/
|
|
28
|
+
export declare function maybeShowReferralTip(opts?: {
|
|
29
|
+
json?: boolean;
|
|
30
|
+
}): void;
|
package/dist/lib/tips.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throttled post-run referral tip (#151) — after a successful `trawl run`,
|
|
3
|
+
* occasionally invite the user to refer a builder. Mirrors
|
|
4
|
+
* lib/updateNotifier.ts's shape: synchronous, instant, self-guarded so a
|
|
5
|
+
* broken tip can never turn a successful command into a failed one.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import config from './config.js';
|
|
9
|
+
/** Max once per 7 days. */
|
|
10
|
+
const TIP_THROTTLE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
|
+
const REFERRAL_TIP_TEXT = 'Invite a builder — they get 500, you get 1,000 compute → trawl.me → account → Referrals';
|
|
12
|
+
/**
|
|
13
|
+
* Opted out via env (presence-based, mirrors NO_COLOR / the update
|
|
14
|
+
* notifier's TRAWL_NO_UPDATE_NOTIFIER — see lib/updateNotifier.ts) or an
|
|
15
|
+
* explicit `false` config value. Absent/undefined config key means enabled.
|
|
16
|
+
*/
|
|
17
|
+
function isOptedOut() {
|
|
18
|
+
if (process.env['TRAWL_NO_TIPS'] !== undefined)
|
|
19
|
+
return true;
|
|
20
|
+
return config.get('tips') === false;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pure gate: true when the tip should print. Every external signal (json
|
|
24
|
+
* flag, TTY-ness, opt-out, persisted timestamp, current time) is a
|
|
25
|
+
* parameter rather than read internally, so this is trivially testable
|
|
26
|
+
* without mocking process.env/stdout/Date/config.
|
|
27
|
+
*/
|
|
28
|
+
export function isReferralTipDue(opts) {
|
|
29
|
+
if (opts.json)
|
|
30
|
+
return false;
|
|
31
|
+
if (!opts.isTTY)
|
|
32
|
+
return false;
|
|
33
|
+
if (opts.optedOut)
|
|
34
|
+
return false;
|
|
35
|
+
return opts.now - opts.lastShownAt >= TIP_THROTTLE_MS;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Best-effort: print one subtle line inviting the user to refer a builder,
|
|
39
|
+
* throttled to once per 7 days via a timestamp persisted in the CLI's
|
|
40
|
+
* existing Conf-backed config store (lib/config.ts — same store as
|
|
41
|
+
* apiUrl/token/telemetry/updateNotifier).
|
|
42
|
+
*
|
|
43
|
+
* This only gates on throttle/TTY/`--json`/opt-out — it does NOT know
|
|
44
|
+
* whether the underlying command actually succeeded. Callers must only
|
|
45
|
+
* invoke it on a genuine success (see commands/scraps.ts `run <id>`'s
|
|
46
|
+
* action, which also checks pollRunProgress's exit code under `--watch`
|
|
47
|
+
* before calling this). Any state read/write failure (corrupt config file,
|
|
48
|
+
* disk error, …) is swallowed silently — a broken tip must never break a
|
|
49
|
+
* run.
|
|
50
|
+
*/
|
|
51
|
+
export function maybeShowReferralTip(opts = {}) {
|
|
52
|
+
try {
|
|
53
|
+
const lastShownAt = config.get('referralTipShownAt') || 0;
|
|
54
|
+
const due = isReferralTipDue({
|
|
55
|
+
json: opts.json,
|
|
56
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
57
|
+
optedOut: isOptedOut(),
|
|
58
|
+
lastShownAt,
|
|
59
|
+
now: Date.now(),
|
|
60
|
+
});
|
|
61
|
+
if (!due)
|
|
62
|
+
return;
|
|
63
|
+
console.log(chalk.dim(REFERRAL_TIP_TEXT));
|
|
64
|
+
config.set('referralTipShownAt', Date.now());
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// silent — #151: a broken tip must never break a run
|
|
68
|
+
}
|
|
69
|
+
}
|
package/docs/agent-quickstart.md
CHANGED
|
@@ -27,7 +27,7 @@ first-class on every one, and none of them ever blocks on a prompt (see
|
|
|
27
27
|
```
|
|
28
28
|
trawl create <url> --prompt <goal> [--no-autofix] [--json] # url also accepted as --url <url> (#116)
|
|
29
29
|
Create a persistent, self-healing scrap from a URL + a goal (AI-generated)
|
|
30
|
-
trawl run <id> [--watch] [--json] Run a scrap
|
|
30
|
+
trawl run <id> [--watch] [--json] Run a scrap (blocks until finished)
|
|
31
31
|
trawl list|ls [--json] [--status <s>] [--limit <n>] [--page <n>] List all scraps
|
|
32
32
|
trawl get <id> [--json] Get scrap details
|
|
33
33
|
trawl data <id> [--json] [--fresh] [--errors] Get scrap data (last persisted run, or --fresh to launch one)
|