@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/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 { fetchUrl } from './commands/fetch.js';
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
- program.addCommand(login);
73
- program.addCommand(logout);
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(telemetry);
119
+ program.addCommand(login);
120
+ program.addCommand(logout);
77
121
  program.addCommand(token);
78
- program.addCommand(fetchUrl);
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 3 call sites that hit those
47
- * endpoints (src/commands/scraps.ts): `scraps run` / `data --fresh`
48
- * (GET /api/scraps/load/:id) and `scraps trigger --wait`
49
- * (POST /api/scraps/worker/:id, synchronous branch only — the default async
50
- * `?wait=false` POST returns almost immediately and keeps the 30s default).
51
- * 300s leaves margin over the ~250s worst case without being unboundedly long.
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 3 call sites that hit those
67
- * endpoints (src/commands/scraps.ts): `scraps run` / `data --fresh`
68
- * (GET /api/scraps/load/:id) and `scraps trigger --wait`
69
- * (POST /api/scraps/worker/:id, synchronous branch only — the default async
70
- * `?wait=false` POST returns almost immediately and keeps the 30s default).
71
- * 300s leaves margin over the ~250s worst case without being unboundedly long.
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": "1.21.0",
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
  ],
@@ -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;
@@ -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
- });