@trawlme/cli 2.0.0 → 2.1.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/README.md +2 -1
- package/dist/commands/create.js +19 -5
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/ping.js +3 -1
- package/dist/commands/scraps.js +18 -27
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +19 -0
- package/dist/lib/spinner.d.ts +18 -0
- package/dist/lib/spinner.js +17 -0
- package/docs/agent-quickstart.md +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,8 @@ All commands accept a global `--debug` flag to show full error stack traces on f
|
|
|
37
37
|
### Core commands (agent + human)
|
|
38
38
|
|
|
39
39
|
```
|
|
40
|
-
trawl create <url> --prompt <goal> [--no-autofix] [--json]
|
|
40
|
+
trawl create <url> --prompt <goal> [--no-autofix] [--json] # url also accepted as --url <url> (#116)
|
|
41
|
+
Create a persistent, self-healing scrap from a URL + a goal (AI-generated)
|
|
41
42
|
trawl run <id> [--watch] [--json] Run a scrap
|
|
42
43
|
trawl list|ls [--json] [--status <success|failure|never|running|regression>] [--limit <n>] [--page <n>]
|
|
43
44
|
trawl get <id> [--json] Get scrap details
|
package/dist/commands/create.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
3
|
+
import { spin } from '../lib/spinner.js';
|
|
4
4
|
import { api, LONG_RUN_TIMEOUT_MS, NetworkError } from '../lib/api.js';
|
|
5
5
|
import { json } from '../lib/format.js';
|
|
6
6
|
import { requireUrl, requireString } from '../lib/validate.js';
|
|
7
|
+
import { UsageError } from '../lib/errors.js';
|
|
7
8
|
/** Best-effort, honest first-run summary — never claims a background retry
|
|
8
9
|
* happened when auto-fix was disabled for this call, and never claims a
|
|
9
10
|
* scrap was persisted when the response carries none (#114-F3 — a hard
|
|
@@ -51,15 +52,28 @@ function scheduleLabel(scrap) {
|
|
|
51
52
|
}
|
|
52
53
|
export const create = new Command('create')
|
|
53
54
|
.description('Create a persistent, self-healing scrap from a URL + a goal (AI-generated)')
|
|
54
|
-
.argument('
|
|
55
|
-
.requiredOption('--prompt <goal>', 'What to extract/scrape, in plain language')
|
|
55
|
+
.argument('[url]', 'Target public URL (http/https)')
|
|
56
|
+
.requiredOption('--prompt <goal>', 'What to extract/scrape, in plain language (required)')
|
|
57
|
+
.option('--url <url>', 'Target public URL — alias of the positional argument')
|
|
56
58
|
.option('--no-autofix', 'Disable AI auto-fix on first-run failure (default: on)')
|
|
57
59
|
.option('--json', 'Output the raw API payload')
|
|
58
60
|
.action(async (rawUrl, opts) => {
|
|
59
61
|
// Fast, local usage-errors (exit 2) — never a round-trip to the server
|
|
60
62
|
// for something we can already tell is bad. Same fail-fast pattern the
|
|
61
63
|
// former `fetch` command used for its URL argument.
|
|
62
|
-
|
|
64
|
+
//
|
|
65
|
+
// #116 — the url is accepted BOTH as the positional argument (agent
|
|
66
|
+
// one-liner) and as `--url` (muscle memory from `scraps create` and the
|
|
67
|
+
// API body {url, goal}). Exactly one is required; both are fine only
|
|
68
|
+
// when identical — two DIFFERENT urls is ambiguous, refuse loudly
|
|
69
|
+
// rather than silently picking one.
|
|
70
|
+
if (rawUrl === undefined && opts.url === undefined) {
|
|
71
|
+
throw new UsageError("missing required argument 'url' (positional, or --url <url>)");
|
|
72
|
+
}
|
|
73
|
+
if (rawUrl !== undefined && opts.url !== undefined && rawUrl !== opts.url) {
|
|
74
|
+
throw new UsageError(`conflicting urls: positional "${rawUrl}" vs --url "${opts.url}" — pass only one`);
|
|
75
|
+
}
|
|
76
|
+
const url = requireUrl(rawUrl ?? opts.url, 'url');
|
|
63
77
|
const goal = requireString(opts.prompt, '--prompt');
|
|
64
78
|
const body = {
|
|
65
79
|
url,
|
|
@@ -81,7 +95,7 @@ export const create = new Command('create')
|
|
|
81
95
|
}
|
|
82
96
|
else {
|
|
83
97
|
try {
|
|
84
|
-
data = await
|
|
98
|
+
data = await spin(call, {
|
|
85
99
|
text: `Creating a scrap from ${url}…`,
|
|
86
100
|
// No verdict symbol here (#106-F3) — ora's success only means "the
|
|
87
101
|
// HTTP call didn't throw", not "the first run succeeded". The real
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { api } from '../lib/api.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { formatDate } from '../lib/format.js';
|
|
3
4
|
/**
|
|
4
5
|
* Known anti-bot vendors (+ auth) that the worker's `blockType` field may name.
|
|
5
6
|
* Ordered by first-match; `blockType` is a freeform worker string, not an enum
|
|
@@ -142,7 +143,7 @@ export function formatDoctor(scrapTitle, run, fix = null, scrapId) {
|
|
|
142
143
|
}
|
|
143
144
|
// Timestamp
|
|
144
145
|
if (run.createdAt) {
|
|
145
|
-
lines.push(chalk.dim(' Run at: ') +
|
|
146
|
+
lines.push(chalk.dim(' Run at: ') + formatDate(run.createdAt));
|
|
146
147
|
}
|
|
147
148
|
// Snapshot hint
|
|
148
149
|
if (run.errorSnapshot?.html || run.statusDetail === 'empty' || run.statusDetail === 'error') {
|
package/dist/commands/ping.js
CHANGED
|
@@ -17,7 +17,9 @@ export const ping = new Command('ping')
|
|
|
17
17
|
json(data);
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
// #119 — label it as the API's version, not a bare `v0.4.0` that reads as
|
|
21
|
+
// "the platform is v0.4" (it's the server package version field).
|
|
22
|
+
const versionSuffix = data.version ? ` (api v${data.version})` : '';
|
|
21
23
|
if (data.status === 'ok') {
|
|
22
24
|
console.log(chalk.green('✓ OK') + versionSuffix);
|
|
23
25
|
}
|
package/dist/commands/scraps.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
3
|
+
import { spin } from '../lib/spinner.js';
|
|
4
4
|
import { api, LONG_RUN_TIMEOUT_MS } from '../lib/api.js';
|
|
5
|
-
import { table, json } from '../lib/format.js';
|
|
5
|
+
import { table, json, formatDate } from '../lib/format.js';
|
|
6
6
|
import { promptPassword } from '../lib/prompt.js';
|
|
7
7
|
import { validateObjectId } from '../lib/validate.js';
|
|
8
8
|
import { classifyError, reportError, UsageError, RefusalError } from '../lib/errors.js';
|
|
@@ -42,16 +42,7 @@ function lastStatus(scrap) {
|
|
|
42
42
|
return last.status === true ? 'success' : 'failure';
|
|
43
43
|
}
|
|
44
44
|
function lastRun(scrap) {
|
|
45
|
-
|
|
46
|
-
if (!last?.createdAt)
|
|
47
|
-
return '—';
|
|
48
|
-
const d = new Date(last.createdAt);
|
|
49
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
50
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
51
|
-
const yy = String(d.getFullYear()).slice(2);
|
|
52
|
-
const hh = String(d.getHours()).padStart(2, '0');
|
|
53
|
-
const min = String(d.getMinutes()).padStart(2, '0');
|
|
54
|
-
return `${dd}/${mm}/${yy} ${hh}:${min}`;
|
|
45
|
+
return formatDate(scrap.history?.[0]?.createdAt); // #119 — unified DD/MM/YY HH:mm
|
|
55
46
|
}
|
|
56
47
|
function statusIcon(status) {
|
|
57
48
|
if (status === 'success')
|
|
@@ -389,7 +380,7 @@ export function attachListCommand(parent, attachOpts = {}) {
|
|
|
389
380
|
}
|
|
390
381
|
let data;
|
|
391
382
|
try {
|
|
392
|
-
data = await
|
|
383
|
+
data = await spin(async () => {
|
|
393
384
|
if (page !== undefined) {
|
|
394
385
|
// Single-page mode: explicit page requested, no loop
|
|
395
386
|
return api.get(`/api/scraps?perPage=50&page=${page}`);
|
|
@@ -445,7 +436,7 @@ export function attachListCommand(parent, attachOpts = {}) {
|
|
|
445
436
|
cron: s.cron || '—',
|
|
446
437
|
status: statusIcon(lastStatus(s)),
|
|
447
438
|
'last run': lastRun(s),
|
|
448
|
-
updated:
|
|
439
|
+
updated: formatDate(s.updatedAt),
|
|
449
440
|
}));
|
|
450
441
|
table(tableRows, ['id', 'title', 'cron', 'status', 'last run', 'updated']);
|
|
451
442
|
// Print footer when --limit truncates
|
|
@@ -471,7 +462,7 @@ export function attachGetCommand(parent, attachOpts = {}) {
|
|
|
471
462
|
console.log(chalk.dim(` Cron: `) + (data.cron || '—'));
|
|
472
463
|
console.log(chalk.dim(` Status: `) + statusIcon(lastStatus(data)));
|
|
473
464
|
console.log(chalk.dim(` Last run: `) + lastRun(data));
|
|
474
|
-
console.log(chalk.dim(` Updated: `) +
|
|
465
|
+
console.log(chalk.dim(` Updated: `) + formatDate(data.updatedAt));
|
|
475
466
|
});
|
|
476
467
|
}
|
|
477
468
|
attachGetCommand(scraps, { hidden: true });
|
|
@@ -566,7 +557,7 @@ scraps
|
|
|
566
557
|
usageError(`Invalid --tier "${opts.tier}" (allowed: ${VALID_TIERS.join(', ')})`, { json: opts.json });
|
|
567
558
|
return;
|
|
568
559
|
}
|
|
569
|
-
const data = await
|
|
560
|
+
const data = await spin(() => api.post('/api/scraps', {
|
|
570
561
|
title: opts.title,
|
|
571
562
|
...(opts.url && { url: opts.url }),
|
|
572
563
|
request: opts.request || '',
|
|
@@ -681,7 +672,7 @@ scraps
|
|
|
681
672
|
console.log(chalk.yellow('Nothing to update. Provide at least one option.'));
|
|
682
673
|
return;
|
|
683
674
|
}
|
|
684
|
-
const data = await
|
|
675
|
+
const data = await spin(() => api.put(`/api/scraps/${id}`, body), {
|
|
685
676
|
text: 'Updating scrap…',
|
|
686
677
|
successText: (d) => `Scrap updated: ${chalk.bold(d._id)}`,
|
|
687
678
|
});
|
|
@@ -735,7 +726,7 @@ export function attachRunCommand(parent, attachOpts = {}) {
|
|
|
735
726
|
// all, mirroring `trawl create`'s own --json handling.
|
|
736
727
|
const data = opts.json
|
|
737
728
|
? await call()
|
|
738
|
-
: await
|
|
729
|
+
: await spin(call, { text: 'Launching scrap…', successText: 'Scrap launched' });
|
|
739
730
|
if (opts.json)
|
|
740
731
|
json(data);
|
|
741
732
|
if (opts.watch) {
|
|
@@ -830,7 +821,7 @@ export function attachDataCommand(parent, attachOpts = {}) {
|
|
|
830
821
|
if (opts.fresh) {
|
|
831
822
|
// #91 P0 — same long-run endpoint as `run` (30-250s server-side);
|
|
832
823
|
// the 30s default was aborting it mid-flight.
|
|
833
|
-
const loaded = await
|
|
824
|
+
const loaded = await spin(() => api.get(`/api/scraps/load/${id}`, { timeoutMs: LONG_RUN_TIMEOUT_MS }), {
|
|
834
825
|
text: 'Launching a fresh scrap run (consumes execute quota)…',
|
|
835
826
|
successText: 'Fresh run complete',
|
|
836
827
|
});
|
|
@@ -1050,7 +1041,7 @@ scraps
|
|
|
1050
1041
|
json({ deleted: true, id });
|
|
1051
1042
|
return;
|
|
1052
1043
|
}
|
|
1053
|
-
await
|
|
1044
|
+
await spin(call, { text: 'Deleting…', successText: 'Scrap deleted' });
|
|
1054
1045
|
});
|
|
1055
1046
|
// banner
|
|
1056
1047
|
scraps
|
|
@@ -1092,7 +1083,7 @@ scraps
|
|
|
1092
1083
|
json(data);
|
|
1093
1084
|
return;
|
|
1094
1085
|
}
|
|
1095
|
-
await
|
|
1086
|
+
await spin(call, {
|
|
1096
1087
|
text: 'Uploading banner…',
|
|
1097
1088
|
successText: `Banner uploaded for scrap ${chalk.bold(id)}`,
|
|
1098
1089
|
});
|
|
@@ -1135,7 +1126,7 @@ export function attachTriggerCommand(parent, attachOpts = {}) {
|
|
|
1135
1126
|
// #107 — under --json the stdout path stays pure: no spinner channel.
|
|
1136
1127
|
const data = opts.json
|
|
1137
1128
|
? await call()
|
|
1138
|
-
: await
|
|
1129
|
+
: await spin(call, {
|
|
1139
1130
|
text: opts.wait ? 'Running worker…' : 'Triggering worker…',
|
|
1140
1131
|
successText: opts.wait ? 'Worker run complete' : 'Worker triggered',
|
|
1141
1132
|
});
|
|
@@ -1214,7 +1205,7 @@ account
|
|
|
1214
1205
|
json(data);
|
|
1215
1206
|
return;
|
|
1216
1207
|
}
|
|
1217
|
-
const data = await
|
|
1208
|
+
const data = await spin(call, { text: 'Saving credentials…', successText: 'Credentials saved' });
|
|
1218
1209
|
const acc = data.account;
|
|
1219
1210
|
console.log(chalk.dim(' Credentials: ') + (acc.hasCredentials ? chalk.green('✓ configured') : chalk.dim('not set')));
|
|
1220
1211
|
console.log(chalk.dim(' Session: ') + (acc.hasSession ? chalk.green('active') : chalk.dim('none')));
|
|
@@ -1244,7 +1235,7 @@ account
|
|
|
1244
1235
|
json({ deleted: true, id });
|
|
1245
1236
|
return;
|
|
1246
1237
|
}
|
|
1247
|
-
await
|
|
1238
|
+
await spin(call, {
|
|
1248
1239
|
text: 'Deleting credentials…',
|
|
1249
1240
|
successText: 'Account credentials deleted',
|
|
1250
1241
|
});
|
|
@@ -1262,7 +1253,7 @@ account
|
|
|
1262
1253
|
json({ cleared: true, id });
|
|
1263
1254
|
return;
|
|
1264
1255
|
}
|
|
1265
|
-
await
|
|
1256
|
+
await spin(call, {
|
|
1266
1257
|
text: 'Clearing session…',
|
|
1267
1258
|
successText: 'Session cleared',
|
|
1268
1259
|
});
|
|
@@ -1311,7 +1302,7 @@ accountSession
|
|
|
1311
1302
|
json(data);
|
|
1312
1303
|
return;
|
|
1313
1304
|
}
|
|
1314
|
-
const data = await
|
|
1305
|
+
const data = await spin(call, {
|
|
1315
1306
|
text: 'Uploading session cookies…',
|
|
1316
1307
|
successText: `Session cookies saved for scrap ${chalk.bold(id)}`,
|
|
1317
1308
|
});
|
|
@@ -1325,7 +1316,7 @@ account
|
|
|
1325
1316
|
.option('--json', 'Output as JSON')
|
|
1326
1317
|
.action(async (id, opts) => {
|
|
1327
1318
|
validateObjectId(id);
|
|
1328
|
-
const data = await
|
|
1319
|
+
const data = await spin(() => api.get(`/api/scraps/${id}`), 'Fetching scrap…');
|
|
1329
1320
|
const acc = data.account;
|
|
1330
1321
|
if (opts.json) {
|
|
1331
1322
|
// #86 finding 12 — `json` is already statically imported at the top of
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
export declare function table(rows: Record<string, unknown>[], columns: string[]): void;
|
|
2
2
|
export declare function json(data: unknown): void;
|
|
3
|
+
/**
|
|
4
|
+
* One date format across the whole CLI: `DD/MM/YY HH:mm` (local time).
|
|
5
|
+
* #119 — `list` mixed `DD/MM/YY` (last-run col) with `M/D/YYYY,
|
|
6
|
+
* h:mm:ss AM/PM` (toLocaleString), ambiguous day/month side by side.
|
|
7
|
+
* Returns `—` for a missing/invalid date.
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatDate(value: string | number | Date | null | undefined): string;
|
package/dist/lib/format.js
CHANGED
|
@@ -23,3 +23,22 @@ export function table(rows, columns) {
|
|
|
23
23
|
export function json(data) {
|
|
24
24
|
console.log(JSON.stringify(data, null, 2));
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* One date format across the whole CLI: `DD/MM/YY HH:mm` (local time).
|
|
28
|
+
* #119 — `list` mixed `DD/MM/YY` (last-run col) with `M/D/YYYY,
|
|
29
|
+
* h:mm:ss AM/PM` (toLocaleString), ambiguous day/month side by side.
|
|
30
|
+
* Returns `—` for a missing/invalid date.
|
|
31
|
+
*/
|
|
32
|
+
export function formatDate(value) {
|
|
33
|
+
if (value === null || value === undefined)
|
|
34
|
+
return '—';
|
|
35
|
+
const d = new Date(value);
|
|
36
|
+
if (Number.isNaN(d.getTime()))
|
|
37
|
+
return '—';
|
|
38
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
39
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
40
|
+
const yy = String(d.getFullYear()).slice(2);
|
|
41
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
42
|
+
const min = String(d.getMinutes()).padStart(2, '0');
|
|
43
|
+
return `${dd}/${mm}/${yy} ${hh}:${min}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type Action<T> = PromiseLike<T> | (() => PromiseLike<T>);
|
|
2
|
+
type SpinOptions<T> = string | {
|
|
3
|
+
text?: string;
|
|
4
|
+
successText?: string | ((result: T) => string);
|
|
5
|
+
failText?: string | ((error: Error) => string);
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Drop-in for `oraPromise` that emits NOTHING when stderr is not a TTY.
|
|
10
|
+
*
|
|
11
|
+
* #119 — under a pipe / non-TTY, `oraPromise` still printed the start text AND
|
|
12
|
+
* a persisted `✔ …` line, so `trawl list | cat` showed the spinner caption
|
|
13
|
+
* twice. An agent/CI never wants spinner chrome; here we just run the action.
|
|
14
|
+
* When stderr IS a TTY, behaviour is identical to `oraPromise` (spinner writes
|
|
15
|
+
* to stderr, stdout stays clean either way).
|
|
16
|
+
*/
|
|
17
|
+
export declare function spin<T>(action: Action<T>, options?: SpinOptions<T>): Promise<T>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { oraPromise } from 'ora';
|
|
2
|
+
/**
|
|
3
|
+
* Drop-in for `oraPromise` that emits NOTHING when stderr is not a TTY.
|
|
4
|
+
*
|
|
5
|
+
* #119 — under a pipe / non-TTY, `oraPromise` still printed the start text AND
|
|
6
|
+
* a persisted `✔ …` line, so `trawl list | cat` showed the spinner caption
|
|
7
|
+
* twice. An agent/CI never wants spinner chrome; here we just run the action.
|
|
8
|
+
* When stderr IS a TTY, behaviour is identical to `oraPromise` (spinner writes
|
|
9
|
+
* to stderr, stdout stays clean either way).
|
|
10
|
+
*/
|
|
11
|
+
export function spin(action, options) {
|
|
12
|
+
if (!process.stderr.isTTY) {
|
|
13
|
+
return Promise.resolve(typeof action === 'function' ? action() : action);
|
|
14
|
+
}
|
|
15
|
+
// oraPromise's own overloads accept (action, string) and (action, options).
|
|
16
|
+
return oraPromise(action, options);
|
|
17
|
+
}
|
package/docs/agent-quickstart.md
CHANGED
|
@@ -25,7 +25,8 @@ first-class on every one, and none of them ever blocks on a prompt (see
|
|
|
25
25
|
[Non-interactive contract](#non-interactive-contract) below):
|
|
26
26
|
|
|
27
27
|
```
|
|
28
|
-
trawl create <url> --prompt <goal> [--no-autofix] [--json]
|
|
28
|
+
trawl create <url> --prompt <goal> [--no-autofix] [--json] # url also accepted as --url <url> (#116)
|
|
29
|
+
Create a persistent, self-healing scrap from a URL + a goal (AI-generated)
|
|
29
30
|
trawl run <id> [--watch] [--json] Run a scrap
|
|
30
31
|
trawl list|ls [--json] [--status <s>] [--limit <n>] [--page <n>] List all scraps
|
|
31
32
|
trawl get <id> [--json] Get scrap details
|