@trawlme/cli 1.21.0 → 2.0.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 +45 -34
- package/dist/commands/create.d.ts +41 -0
- package/dist/commands/create.js +146 -0
- package/dist/commands/scraps.d.ts +30 -0
- package/dist/commands/scraps.js +413 -392
- package/dist/index.d.ts +8 -0
- package/dist/index.js +50 -8
- package/dist/lib/api.d.ts +9 -6
- package/dist/lib/api.js +9 -6
- package/docs/agent-quickstart.md +121 -0
- package/package.json +2 -1
- package/dist/commands/fetch.d.ts +0 -31
- package/dist/commands/fetch.js +0 -95
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ import { Command } from 'commander';
|
|
|
15
15
|
* chain — matching the pre-existing convention that a direct child of the
|
|
16
16
|
* root (e.g. `scraps list`, `telemetry on`) is named relative to its
|
|
17
17
|
* immediate group, never prefixed with the program name.
|
|
18
|
+
*
|
|
19
|
+
* #108 note: promoting a verb to a top-level command (see `createProgram`
|
|
20
|
+
* below) renamed ITS resolved telemetry name from `scraps <verb>` to
|
|
21
|
+
* `<verb>` — the canonical top-level attach and the legacy hidden
|
|
22
|
+
* `scraps <verb>` attach are two separate Command instances (scraps.ts's
|
|
23
|
+
* double-attach factories), each with its own parent chain, so they
|
|
24
|
+
* resolve to two different names here even though they run the same
|
|
25
|
+
* handler. Intentional (the canonical command IS now `<verb>`), not a bug.
|
|
18
26
|
*/
|
|
19
27
|
export declare function resolveCommandName(actionCommand: Command | undefined): string;
|
|
20
28
|
/**
|
package/dist/index.js
CHANGED
|
@@ -4,11 +4,11 @@ import { readFileSync, realpathSync } from 'node:fs';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { login, logout } from './commands/login.js';
|
|
7
|
-
import { scraps } from './commands/scraps.js';
|
|
7
|
+
import { scraps, attachListCommand, attachGetCommand, attachRunCommand, attachDataCommand, attachHistoryCommand, attachRunInfoCommand, attachTriggerCommand, } from './commands/scraps.js';
|
|
8
8
|
import { skills } from './commands/skills.js';
|
|
9
9
|
import { telemetry } from './commands/telemetry.js';
|
|
10
10
|
import { token } from './commands/token.js';
|
|
11
|
-
import {
|
|
11
|
+
import { create } from './commands/create.js';
|
|
12
12
|
import { whoami } from './commands/whoami.js';
|
|
13
13
|
import { ping } from './commands/ping.js';
|
|
14
14
|
import { autoUpdateInstalledSkills } from './lib/skills.js';
|
|
@@ -32,6 +32,14 @@ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8
|
|
|
32
32
|
* chain — matching the pre-existing convention that a direct child of the
|
|
33
33
|
* root (e.g. `scraps list`, `telemetry on`) is named relative to its
|
|
34
34
|
* immediate group, never prefixed with the program name.
|
|
35
|
+
*
|
|
36
|
+
* #108 note: promoting a verb to a top-level command (see `createProgram`
|
|
37
|
+
* below) renamed ITS resolved telemetry name from `scraps <verb>` to
|
|
38
|
+
* `<verb>` — the canonical top-level attach and the legacy hidden
|
|
39
|
+
* `scraps <verb>` attach are two separate Command instances (scraps.ts's
|
|
40
|
+
* double-attach factories), each with its own parent chain, so they
|
|
41
|
+
* resolve to two different names here even though they run the same
|
|
42
|
+
* handler. Intentional (the canonical command IS now `<verb>`), not a bug.
|
|
35
43
|
*/
|
|
36
44
|
export function resolveCommandName(actionCommand) {
|
|
37
45
|
if (!actionCommand)
|
|
@@ -63,21 +71,55 @@ export function collectCommandNames(root) {
|
|
|
63
71
|
walk(root);
|
|
64
72
|
return names;
|
|
65
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* #108 — surface reorg into two `trawl --help` tiers. Core verbs are
|
|
76
|
+
* agent+human, `--json` first-class, non-interactive; Management is the
|
|
77
|
+
* existing human/CI surface, kept but grouped so top-level help reads
|
|
78
|
+
* simple. Commander v14's native per-command help group (`.commandsGroup()`
|
|
79
|
+
* sets the default a subsequently-registered command inherits via
|
|
80
|
+
* `.helpGroup()`) drives the section headings — group ORDER in the printed
|
|
81
|
+
* help follows first-seen insertion order into `program.commands`, so every
|
|
82
|
+
* Core command is registered below before any Management one.
|
|
83
|
+
*/
|
|
84
|
+
const CORE_GROUP = 'Core commands (agent + human):';
|
|
85
|
+
const MANAGEMENT_GROUP = 'Management commands (human/CI):';
|
|
66
86
|
export function createProgram() {
|
|
67
87
|
const program = new Command()
|
|
68
88
|
.name('trawl')
|
|
69
89
|
.description('Trawl CLI — manage scraps from the terminal')
|
|
70
90
|
.version(pkg.version)
|
|
71
91
|
.option('--debug', 'Show full error stack traces');
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
// Core verbs (#108) — promoted/listed first: create, run, list, get, data,
|
|
93
|
+
// history, run-info, trigger, whoami, ping. `list`/`get`/`run`/`data`/
|
|
94
|
+
// `history`/`run-info`/`trigger` are built via scraps.ts's exported
|
|
95
|
+
// attachXCommand() factories — the SAME definition also stays wired
|
|
96
|
+
// (hidden) under `scraps` there, so every pre-#108 `trawl scraps <verb>`
|
|
97
|
+
// invocation keeps resolving (no breaking change).
|
|
98
|
+
//
|
|
99
|
+
// #114 — `create` replaced `fetch` in this slot: `POST /api/ai/wizard`
|
|
100
|
+
// (AI-generate + persist + first-run + autofix), a distinct command from
|
|
101
|
+
// the still-untouched `trawl scraps create` (raw-script management verb).
|
|
102
|
+
program.commandsGroup(CORE_GROUP);
|
|
103
|
+
program.addCommand(create);
|
|
104
|
+
attachRunCommand(program);
|
|
105
|
+
attachListCommand(program);
|
|
106
|
+
attachGetCommand(program);
|
|
107
|
+
attachDataCommand(program);
|
|
108
|
+
attachHistoryCommand(program);
|
|
109
|
+
attachRunInfoCommand(program);
|
|
110
|
+
attachTriggerCommand(program);
|
|
111
|
+
program.addCommand(whoami);
|
|
112
|
+
program.addCommand(ping);
|
|
113
|
+
// Management (#108) — human/CI surface, grouped below. `scraps` still
|
|
114
|
+
// holds every pre-#108 management command (create/update/delete/banner/
|
|
115
|
+
// watch/account.*/session.*/doctor/autofix/snapshot) exactly as before.
|
|
116
|
+
program.commandsGroup(MANAGEMENT_GROUP);
|
|
74
117
|
program.addCommand(scraps);
|
|
75
118
|
program.addCommand(skills);
|
|
76
|
-
program.addCommand(
|
|
119
|
+
program.addCommand(login);
|
|
120
|
+
program.addCommand(logout);
|
|
77
121
|
program.addCommand(token);
|
|
78
|
-
program.addCommand(
|
|
79
|
-
program.addCommand(whoami);
|
|
80
|
-
program.addCommand(ping);
|
|
122
|
+
program.addCommand(telemetry);
|
|
81
123
|
return program;
|
|
82
124
|
}
|
|
83
125
|
/**
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -43,12 +43,15 @@ export declare function notLoggedInError(): AuthError;
|
|
|
43
43
|
* dry-run retries). The generic 30s default was aborting those mid-flight
|
|
44
44
|
* and surfacing a fabricated `NetworkError timed out` (exit 5) for a request
|
|
45
45
|
* that was always going to succeed given enough time. Passed as the per-call
|
|
46
|
-
* `{timeoutMs}` override at exactly the
|
|
47
|
-
* endpoints
|
|
48
|
-
* (
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
46
|
+
* `{timeoutMs}` override at exactly the 4 call sites that hit those
|
|
47
|
+
* endpoints: `scraps run` / `data --fresh` (GET /api/scraps/load/:id) and
|
|
48
|
+
* `scraps trigger --wait` (POST /api/scraps/worker/:id, synchronous branch
|
|
49
|
+
* only — the default async `?wait=false` POST returns almost immediately and
|
|
50
|
+
* keeps the 30s default) — all three in src/commands/scraps.ts — plus (#114)
|
|
51
|
+
* `create` (POST /api/ai/wizard, src/commands/create.ts), whose AI-generation
|
|
52
|
+
* + scrap creation + first run + autofix pipeline runs the same 30–250s+
|
|
53
|
+
* server-side. 300s leaves margin over the ~250s worst case without being
|
|
54
|
+
* unboundedly long.
|
|
52
55
|
*/
|
|
53
56
|
export declare const LONG_RUN_TIMEOUT_MS = 300000;
|
|
54
57
|
export interface RequestOptions {
|
package/dist/lib/api.js
CHANGED
|
@@ -63,12 +63,15 @@ const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
63
63
|
* dry-run retries). The generic 30s default was aborting those mid-flight
|
|
64
64
|
* and surfacing a fabricated `NetworkError timed out` (exit 5) for a request
|
|
65
65
|
* that was always going to succeed given enough time. Passed as the per-call
|
|
66
|
-
* `{timeoutMs}` override at exactly the
|
|
67
|
-
* endpoints
|
|
68
|
-
* (
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
66
|
+
* `{timeoutMs}` override at exactly the 4 call sites that hit those
|
|
67
|
+
* endpoints: `scraps run` / `data --fresh` (GET /api/scraps/load/:id) and
|
|
68
|
+
* `scraps trigger --wait` (POST /api/scraps/worker/:id, synchronous branch
|
|
69
|
+
* only — the default async `?wait=false` POST returns almost immediately and
|
|
70
|
+
* keeps the 30s default) — all three in src/commands/scraps.ts — plus (#114)
|
|
71
|
+
* `create` (POST /api/ai/wizard, src/commands/create.ts), whose AI-generation
|
|
72
|
+
* + scrap creation + first run + autofix pipeline runs the same 30–250s+
|
|
73
|
+
* server-side. 300s leaves margin over the ~250s worst case without being
|
|
74
|
+
* unboundedly long.
|
|
72
75
|
*/
|
|
73
76
|
export const LONG_RUN_TIMEOUT_MS = 300_000;
|
|
74
77
|
/**
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Trawl CLI — Agent Quickstart
|
|
2
|
+
|
|
3
|
+
The minimal surface an AI agent needs to drive `@trawlme/cli` non-interactively.
|
|
4
|
+
For the full command reference (management surface, Claude Code skills,
|
|
5
|
+
telemetry, etc.) see the [main README](../README.md) — the human/CI guide.
|
|
6
|
+
|
|
7
|
+
## Auth — zero prompts
|
|
8
|
+
|
|
9
|
+
Set `TRAWL_TOKEN` and every command authenticates without ever touching a
|
|
10
|
+
prompt:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
export TRAWL_TOKEN=<jwt>
|
|
14
|
+
trawl whoami --json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
(Interactive `trawl login` and the `--url`/config-file flow are documented in
|
|
18
|
+
the README's [Authentication](../README.md#authentication) section — an
|
|
19
|
+
agent should never need them.)
|
|
20
|
+
|
|
21
|
+
## Core commands (agent + human)
|
|
22
|
+
|
|
23
|
+
These ten commands are the CLI's agent+human surface — `--json` is
|
|
24
|
+
first-class on every one, and none of them ever blocks on a prompt (see
|
|
25
|
+
[Non-interactive contract](#non-interactive-contract) below):
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
trawl create <url> --prompt <goal> [--no-autofix] [--json] Create a persistent, self-healing scrap from a URL + a goal (AI-generated)
|
|
29
|
+
trawl run <id> [--watch] [--json] Run a scrap
|
|
30
|
+
trawl list|ls [--json] [--status <s>] [--limit <n>] [--page <n>] List all scraps
|
|
31
|
+
trawl get <id> [--json] Get scrap details
|
|
32
|
+
trawl data <id> [--json] [--fresh] [--errors] Get scrap data (last persisted run, or --fresh to launch one)
|
|
33
|
+
trawl history <id> [--json] [-n <limit>] List past runs for a scrap
|
|
34
|
+
trawl run-info <hid> [--json] Show details of a single run
|
|
35
|
+
trawl trigger <id> [--watch] [--wait] [--json] Launch a scrap as a background worker
|
|
36
|
+
trawl whoami [--json] Show the authenticated user's identity
|
|
37
|
+
trawl ping [--json] Health/version handshake against the Trawl API
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`trawl create` is the closest primitive to "turn this URL + goal into a
|
|
41
|
+
working scrap" — it runs the AI wizard server-side (`POST /api/ai/wizard`):
|
|
42
|
+
generate scrap code from `--prompt` via LLM, persist the scrap, trigger its
|
|
43
|
+
first run, and auto-fix on failure (default on; `--no-autofix` disables it).
|
|
44
|
+
`success` is an honest outcome of that first run, not "did the HTTP call
|
|
45
|
+
succeed" — a failed first run is still a 200 response (the scrap was still
|
|
46
|
+
created), and the CLI exits `1` in that case even though `--json` always
|
|
47
|
+
prints the raw payload verbatim. The call legitimately runs 30–250s+
|
|
48
|
+
server-side (AI generation + a real run) — the CLI arms the same long-run
|
|
49
|
+
timeout `run`/`data --fresh`/`trigger --wait` use instead of the generic 30s
|
|
50
|
+
default.
|
|
51
|
+
|
|
52
|
+
> **Not idempotent, and not a one-off run.** A client-side timeout (exit `5`,
|
|
53
|
+
> a `NetworkError`) does not mean the wizard failed server-side — scrap
|
|
54
|
+
> creation + the first run keep going after the CLI gives up waiting, so the
|
|
55
|
+
> scrap may already exist. Check `trawl list --json` for a matching URL/title
|
|
56
|
+
> **before** retrying — a blind retry creates a duplicate scrap and burns
|
|
57
|
+
> quota a second time for the same goal. Separately, the created scrap is
|
|
58
|
+
> scheduled to re-run every day at 07:00 UTC by default
|
|
59
|
+
> (`cron: "0 7 * * *"`, unrelated to `--no-autofix`) — each recurring run
|
|
60
|
+
> consumes execute quota. Disable or change it once you've reviewed the
|
|
61
|
+
> scrap: `trawl scraps update <id> --no-cron` (or `--cron <expr>`). Finally,
|
|
62
|
+
> if `TRAWL_TIMEOUT` is set globally for a tighter budget than 300s, it
|
|
63
|
+
> clamps `create`'s ceiling too (env always wins) — unset it or raise it
|
|
64
|
+
> before calling `create`.
|
|
65
|
+
|
|
66
|
+
> **No breaking change:** every verb above is also still reachable under its
|
|
67
|
+
> pre-reorg path, `trawl scraps <verb>` (e.g. `trawl scraps list`) — kept as
|
|
68
|
+
> a hidden alias. Prefer the bare top-level form above; it's what
|
|
69
|
+
> `trawl --help` now shows.
|
|
70
|
+
|
|
71
|
+
For the full flag reference (tier overrides on `scraps create`/`scraps
|
|
72
|
+
update` — the core `create` verb above has no `--tier` of its own, the
|
|
73
|
+
`--watch` polling mechanics, retention/regression semantics on `data`, …)
|
|
74
|
+
see the README's [Core commands](../README.md#core-commands-agent--human) section
|
|
75
|
+
— this doc intentionally stays minimal.
|
|
76
|
+
|
|
77
|
+
## `--json` contract
|
|
78
|
+
|
|
79
|
+
Every command above supports `--json`: a single structured payload on
|
|
80
|
+
stdout, nothing else. Two narrow exceptions carried over from the human
|
|
81
|
+
surface: a `--watch` poll emits exactly one final NDJSON line once the run
|
|
82
|
+
reaches a terminal state (not the whole progress stream), and there is no
|
|
83
|
+
JSON form of an HTML page (irrelevant to the core verbs above — that only
|
|
84
|
+
applies to the management-only `scraps snapshot`).
|
|
85
|
+
|
|
86
|
+
On failure, `--json` emits a single error envelope on stdout instead of
|
|
87
|
+
prose — `{"error":{"message","status?","kind"}}` — and the human-readable
|
|
88
|
+
line goes to stderr, never stdout. `kind` is the machine-readable
|
|
89
|
+
discriminant (`"usage"`/`"auth"`/`"not_found"`/`"network"`/`"api"`/
|
|
90
|
+
`"refused"`/`"unknown"`) a script should switch on.
|
|
91
|
+
|
|
92
|
+
## Non-interactive contract
|
|
93
|
+
|
|
94
|
+
No core verb ever blocks waiting for a prompt. When stdin/stdout isn't a
|
|
95
|
+
real TTY (any subprocess-driven invocation) — or `--json` is set — any
|
|
96
|
+
command that would otherwise ask a `[y/N]` confirmation or a missing value
|
|
97
|
+
instead fails fast with a structured usage error (exit `2`) rather than
|
|
98
|
+
hanging. Full rule + rationale: README's
|
|
99
|
+
[Non-interactive rule](../README.md#non-interactive-rule).
|
|
100
|
+
|
|
101
|
+
## Exit codes
|
|
102
|
+
|
|
103
|
+
| Code | Meaning |
|
|
104
|
+
|------|---------|
|
|
105
|
+
| `0` | Success |
|
|
106
|
+
| `1` | Unknown/generic error, or a business-logic outcome (e.g. `create`'s honest `success:false` first-run outcome, `data`'s `run_failed`/`in_progress`) |
|
|
107
|
+
| `2` | Usage error (bad flag/value, invalid ID, missing required argument, or the non-interactive guard refusing to prompt) |
|
|
108
|
+
| `3` | Auth error (not logged in, or the session token is expired/invalid) |
|
|
109
|
+
| `4` | Not found (no such resource, or no persisted payload to read) |
|
|
110
|
+
| `5` | Network error (API host unreachable, DNS/connection/TLS failure, or timeout) |
|
|
111
|
+
|
|
112
|
+
This table is the stable contract; per-command nuance and overloads (e.g.
|
|
113
|
+
`create`'s domain-level failure sharing exit `1` with an unmapped bug) are
|
|
114
|
+
documented once, in the README's [Exit codes](../README.md#exit-codes)
|
|
115
|
+
section — treat that as canonical if the two ever seem to disagree.
|
|
116
|
+
|
|
117
|
+
## Minimal example
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
TRAWL_TOKEN=<jwt> trawl create https://example.com --prompt "Extract the article title and body text" --json
|
|
121
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trawlme/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Trawl CLI — manage scraps from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
+
"docs",
|
|
11
12
|
"README.md",
|
|
12
13
|
"LICENSE"
|
|
13
14
|
],
|
package/dist/commands/fetch.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
/**
|
|
3
|
-
* `POST /api/scraps/fetch-url` response contract (#1697, trawl_node —
|
|
4
|
-
* shipped, contract LOCKED). REST counterpart of the MCP `trawl_fetch_url`
|
|
5
|
-
* tool: both run through the same shared engine
|
|
6
|
-
* (scraps.fetchUrl.service.js#runEphemeralUrlFetch), so the outcome shape is
|
|
7
|
-
* identical regardless of transport.
|
|
8
|
-
*
|
|
9
|
-
* - `status`: 'completed' | 'failed' | 'empty' | 'blocked' — an HONEST
|
|
10
|
-
* outcome, not a bare "it ran". A failed/blocked/empty run is still a
|
|
11
|
-
* 200 response (this is not an HTTP error), so callers must branch on
|
|
12
|
-
* `status`, never assume 2xx means "got data".
|
|
13
|
-
* - `error` is present ONLY when `status === 'failed'`.
|
|
14
|
-
* - `truncated: true` when the 100KB response-payload cap tripped — the
|
|
15
|
-
* server drops `result` in that case and repoints `url` at the full
|
|
16
|
-
* history row (`/api/historys/:runId`) instead of echoing the fetched
|
|
17
|
-
* target.
|
|
18
|
-
*/
|
|
19
|
-
export interface FetchUrlResponse {
|
|
20
|
-
url: string;
|
|
21
|
-
status: 'completed' | 'failed' | 'empty' | 'blocked';
|
|
22
|
-
runId?: string | null;
|
|
23
|
-
statusDetail?: string | null;
|
|
24
|
-
blocked?: boolean;
|
|
25
|
-
length?: number | null;
|
|
26
|
-
result?: unknown;
|
|
27
|
-
truncated?: boolean;
|
|
28
|
-
reason?: string;
|
|
29
|
-
error?: string;
|
|
30
|
-
}
|
|
31
|
-
export declare const fetchUrl: Command;
|
package/dist/commands/fetch.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { oraPromise } from 'ora';
|
|
4
|
-
import { api, LONG_RUN_TIMEOUT_MS } from '../lib/api.js';
|
|
5
|
-
import { json } from '../lib/format.js';
|
|
6
|
-
import { requireUrl } from '../lib/validate.js';
|
|
7
|
-
function statusIcon(status) {
|
|
8
|
-
if (status === 'completed')
|
|
9
|
-
return chalk.green('✓');
|
|
10
|
-
if (status === 'failed')
|
|
11
|
-
return chalk.red('✗');
|
|
12
|
-
if (status === 'blocked')
|
|
13
|
-
return chalk.yellow('⚠');
|
|
14
|
-
return chalk.dim('•'); // empty
|
|
15
|
-
}
|
|
16
|
-
/** Best-effort human summary of `result` — same "count + first-item keys"
|
|
17
|
-
* shape scraps.ts's renderScrapItems uses, never a full dump (that's what
|
|
18
|
-
* --json is for). */
|
|
19
|
-
function renderResultSummary(result) {
|
|
20
|
-
if (!Array.isArray(result))
|
|
21
|
-
return;
|
|
22
|
-
console.log(chalk.dim(` Items: `) + result.length);
|
|
23
|
-
const first = result[0];
|
|
24
|
-
if (result.length > 0 && first && typeof first === 'object') {
|
|
25
|
-
console.log(chalk.dim(` First keys: `) + Object.keys(first).join(', '));
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
export const fetchUrl = new Command('fetch')
|
|
29
|
-
.description('One-shot fetch + extract readable content from a public URL (no scrap needed)')
|
|
30
|
-
.argument('<url>', 'Target public HTTPS URL')
|
|
31
|
-
.option('--json', 'Output the raw API payload')
|
|
32
|
-
.option('--reason <reason>', 'Audit-trail reason for this fetch (logged server-side, max 500 chars)')
|
|
33
|
-
.action(async (rawUrl, opts) => {
|
|
34
|
-
// Fast, local usage-error (exit 2) on an obviously malformed URL — never
|
|
35
|
-
// a round-trip to the server for something we can already tell is bad.
|
|
36
|
-
// The server still re-validates (SSRF guard) — this is a UX fast-path,
|
|
37
|
-
// not a security boundary.
|
|
38
|
-
const url = requireUrl(rawUrl, 'url');
|
|
39
|
-
const body = {
|
|
40
|
-
url,
|
|
41
|
-
...(opts.reason !== undefined && { reason: opts.reason }),
|
|
42
|
-
};
|
|
43
|
-
// #106 review F1 — this endpoint runs the FULL worker pipeline (browser
|
|
44
|
-
// `goto` networkidle2 + extraction + persistence), legitimately 30-250s
|
|
45
|
-
// server-side — same #91 P0 pattern as `scraps run` / `data --fresh` /
|
|
46
|
-
// `trigger --wait` (src/commands/scraps.ts). The 30s DEFAULT_TIMEOUT_MS
|
|
47
|
-
// was aborting it mid-flight and would have surfaced a fabricated
|
|
48
|
-
// NetworkError timeout for a request that was always going to succeed.
|
|
49
|
-
const call = () => api.post('/api/scraps/fetch-url', body, { timeoutMs: LONG_RUN_TIMEOUT_MS });
|
|
50
|
-
// #106 review F2 — under --json the stdout path must be provably pure:
|
|
51
|
-
// no spinner channel at all. Only the human path gets the ora progress
|
|
52
|
-
// indicator; --json calls the API directly.
|
|
53
|
-
const data = opts.json
|
|
54
|
-
? await call()
|
|
55
|
-
: await oraPromise(call, {
|
|
56
|
-
text: `Fetching ${url}…`,
|
|
57
|
-
// #106 review F3 — no successText verdict here. ora's success
|
|
58
|
-
// symbol only means "the HTTP call didn't throw", not "the fetch
|
|
59
|
-
// succeeded" — a failed/blocked domain `status` is still a 200
|
|
60
|
-
// response. The real outcome is rendered below via statusIcon +
|
|
61
|
-
// the Status: line; a green check here would contradict a
|
|
62
|
-
// red/yellow icon printed right after it.
|
|
63
|
-
successText: 'Request complete',
|
|
64
|
-
});
|
|
65
|
-
if (opts.json) {
|
|
66
|
-
json(data);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
console.log(`${statusIcon(data.status)} ${chalk.bold(data.url)}`);
|
|
70
|
-
console.log(chalk.dim(` Status: `) + data.status);
|
|
71
|
-
if (data.runId)
|
|
72
|
-
console.log(chalk.dim(` Run ID: `) + data.runId);
|
|
73
|
-
if (data.statusDetail)
|
|
74
|
-
console.log(chalk.dim(` Detail: `) + data.statusDetail);
|
|
75
|
-
if (data.length != null)
|
|
76
|
-
console.log(chalk.dim(` Length: `) + data.length);
|
|
77
|
-
if (data.truncated) {
|
|
78
|
-
console.log(chalk.yellow(` ⚠ Truncated (payload too large) — full result: ${data.url}`));
|
|
79
|
-
}
|
|
80
|
-
if (data.status === 'failed' && data.error) {
|
|
81
|
-
console.log(chalk.red(` Error: `) + data.error);
|
|
82
|
-
}
|
|
83
|
-
renderResultSummary(data.result);
|
|
84
|
-
}
|
|
85
|
-
// Honest exit code alongside the honest payload — a --json caller gets
|
|
86
|
-
// the raw body regardless (never wrapped/altered), but a script checking
|
|
87
|
-
// the exit code alone must be able to tell "no usable data" from "ran
|
|
88
|
-
// fine" without parsing. `blocked` (#106 review F4 — an antibot wall) is
|
|
89
|
-
// a failure to get data exactly like `failed`: an agent scripting
|
|
90
|
-
// `trawl fetch ... || handle` must see non-zero for either. `empty`
|
|
91
|
-
// stays 0 on purpose — the fetch genuinely ran to completion, there was
|
|
92
|
-
// just nothing extractable at that URL; that's not an error.
|
|
93
|
-
if (data.status === 'failed' || data.status === 'blocked')
|
|
94
|
-
process.exitCode = 1;
|
|
95
|
-
});
|