@trawlme/cli 2.1.1 → 2.2.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 CHANGED
@@ -46,6 +46,8 @@ trawl data <id> [--json] [--fresh] [--errors] Get scrap data (last persisted ru
46
46
  trawl history <id> [--json] [-n <limit>] List past runs for a scrap (newest first)
47
47
  trawl run-info <hid> [--json] Show details of a single run
48
48
  trawl trigger <id> [--watch] [--wait] [--json] Launch a scrap as a background worker (returns immediately)
49
+ trawl 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>] [--json]
50
+ Update an existing scrap
49
51
  trawl whoami [--json] Show the authenticated user's identity
50
52
  trawl ping [--json] Health/version handshake against the Trawl API
51
53
  ```
@@ -54,7 +56,7 @@ trawl ping [--json] Health/version handshake against
54
56
 
55
57
  `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`). `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.
56
58
 
57
- > **`create` is NOT idempotent, and every wizard-created scrap runs on a DAILY cron by default.** A client-side timeout (exit `5`, a `NetworkError`) does not mean the wizard failed server-side — scrap creation + the first run keep going after the CLI gives up waiting, so the scrap may already exist. Run `trawl list` and look for a matching URL/title **before** retrying — a blind retry creates a DUPLICATE scrap and burns AI-generation quota a second time for the same goal. Separately, the scrap the wizard creates is scheduled to re-run every day at 07:00 UTC (`cron: "0 7 * * *"`, hardcoded server-side, unrelated to `--no-autofix`) — each of those recurring runs consumes execute quota like any other run. Review the generated scrap, then change or disable the schedule with `trawl scraps update <id> --cron <expr>` (or `--no-cron` to disable it). Because the call can legitimately run 250s+, also confirm `TRAWL_TIMEOUT` isn't set to something tighter than `create` needs — the env var always wins over `create`'s own 300s default (see [Environment variables](#environment-variables)), so a value set for another purpose (e.g. a tight CI smoke-test budget) silently clamps `create` too; unset it or raise it before running `create`.
59
+ > **`create` is NOT idempotent, and every wizard-created scrap runs on a DAILY cron by default.** A client-side timeout (exit `5`, a `NetworkError`) does not mean the wizard failed server-side — scrap creation + the first run keep going after the CLI gives up waiting, so the scrap may already exist. Run `trawl list` and look for a matching URL/title **before** retrying — a blind retry creates a DUPLICATE scrap and burns AI-generation quota a second time for the same goal. Separately, the scrap the wizard creates is scheduled to re-run every day at 07:00 UTC (`cron: "0 7 * * *"`, hardcoded server-side, unrelated to `--no-autofix`) — each of those recurring runs consumes execute quota like any other run. Review the generated scrap, then change or disable the schedule with `trawl update <id> --cron <expr>` (or `--no-cron` to disable it). Because the call can legitimately run 250s+, also confirm `TRAWL_TIMEOUT` isn't set to something tighter than `create` needs — the env var always wins over `create`'s own 300s default (see [Environment variables](#environment-variables)), so a value set for another purpose (e.g. a tight CI smoke-test budget) silently clamps `create` too; unset it or raise it before running `create`.
58
60
 
59
61
  - `list` has a short alias, `ls` (matches `trawl --help`'s `list|ls`).
60
62
  - `history` lists past runs (newest first); `run-info <hid>` shows details of a single run from that history.
@@ -68,7 +70,6 @@ trawl ping [--json] Health/version handshake against
68
70
 
69
71
  ```
70
72
  trawl scraps create -t <title> [-u <url>] [-r <request>] [-d <description>] [--tier <tier0|tier1|tier2|tier3|tier4>] [--json]
71
- 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>] [--json]
72
73
  trawl scraps watch <id> [--json]
73
74
  trawl scraps doctor <id> [--json] [--autofix]
74
75
  trawl scraps autofix <id> [--json]
@@ -30,7 +30,8 @@ export interface WizardResponse {
30
30
  * 'UTC'` — unconditionally, regardless of `--prompt`/`--no-autofix`.
31
31
  * Present on the scrap object returned here (the wizard controller
32
32
  * passes the created scrap straight through, no stripping) — change or
33
- * disable it with `trawl scraps update <id> --cron <expr>` / `--no-cron`.
33
+ * disable it with `trawl update <id> --cron <expr>` / `--no-cron` (#120
34
+ * — `update` is now a promoted top-level verb, same as `get`/`run`/…).
34
35
  */
35
36
  cron?: string | null;
36
37
  cronTimezone?: string;
@@ -140,6 +140,7 @@ export declare function pollRunProgress(id: string, before: BeforeRunState | und
140
140
  }): Promise<void>;
141
141
  export declare function attachListCommand(parent: Command, attachOpts?: AttachOptions): Command;
142
142
  export declare function attachGetCommand(parent: Command, attachOpts?: AttachOptions): Command;
143
+ export declare function attachUpdateCommand(parent: Command, attachOpts?: AttachOptions): Command;
143
144
  export declare function attachRunCommand(parent: Command, attachOpts?: AttachOptions): Command;
144
145
  export declare function attachDataCommand(parent: Command, attachOpts?: AttachOptions): Command;
145
146
  export declare function attachHistoryCommand(parent: Command, attachOpts?: AttachOptions): Command;
@@ -583,129 +583,132 @@ scraps
583
583
  if (refused)
584
584
  process.exitCode = 1;
585
585
  });
586
- // update
587
- scraps
588
- .command('update <id>')
589
- .description('Update an existing scrap')
590
- .option('-t, --title <title>', 'New title')
591
- .option('-u, --url <url>', 'New target URL')
592
- .option('-r, --request <request>', 'New request')
593
- .option('-d, --description <text>', 'New description')
594
- .option('--cron <expression>', 'Cron expression (empty string to disable)')
595
- .option('--no-cron', 'Disable cron (set to null)')
596
- .option('--alert <email>', 'Failure alert email (empty string to clear)')
597
- .option('--no-alert', 'Disable failure alert email (set to null)')
598
- .option('--autofix', 'Enable AI Fix (auto-recovery on selector breakage)')
599
- .option('--no-autofix', 'Disable AI Fix')
600
- .option('-p, --params <json>', 'Runtime params as JSON array of objects (e.g. \'[{"TRAWL.paramName":"value"}]\')')
601
- .option('--params-file <path>', 'Runtime params from a JSON file')
602
- .option('--tier <tier>', `Force proxy tier (${VALID_TIERS.join('|')})`)
603
- .option('--force-tier <tier>', `Raise the proxy-tier ceiling PAST the auto-cap (${VALID_TIERS.join('|')}) — history-gated: may be refused or cost more`)
604
- .option('--json', 'Output as JSON')
605
- .action(async (id, opts) => {
606
- validateObjectId(id);
607
- if (opts.tier !== undefined && !VALID_TIERS.includes(opts.tier)) {
608
- usageError(`Invalid --tier "${opts.tier}" (allowed: ${VALID_TIERS.join(', ')})`, { json: opts.json });
609
- return;
610
- }
611
- if (opts.forceTier !== undefined && !VALID_TIERS.includes(opts.forceTier)) {
612
- usageError(`Invalid --force-tier "${opts.forceTier}" (allowed: ${VALID_TIERS.join(', ')})`, { json: opts.json });
613
- return;
614
- }
615
- const body = {};
616
- if (opts.title !== undefined)
617
- body.title = opts.title;
618
- if (opts.url !== undefined)
619
- body.url = opts.url;
620
- if (opts.request !== undefined)
621
- body.request = opts.request;
622
- if (opts.description !== undefined)
623
- body.description = opts.description;
624
- if (opts.cron === false)
625
- body.cron = null;
626
- else if (typeof opts.cron === 'string')
627
- body.cron = opts.cron === '' ? null : opts.cron;
628
- if (opts.alert === false)
629
- body.alert = null;
630
- else if (typeof opts.alert === 'string')
631
- body.alert = opts.alert === '' ? null : opts.alert;
632
- if (opts.autofix === true)
633
- body.autoFix = true;
634
- else if (opts.autofix === false)
635
- body.autoFix = false;
636
- if (opts.params !== undefined || opts.paramsFile !== undefined) {
637
- let raw;
638
- if (opts.paramsFile) {
639
- const { readFileSync } = await import('fs');
640
- raw = readFileSync(opts.paramsFile, 'utf8');
586
+ // update — promoted to a top-level verb (#108/#120)
587
+ export function attachUpdateCommand(parent, attachOpts = {}) {
588
+ return parent
589
+ .command('update <id>', attachOpts)
590
+ .description('Update an existing scrap')
591
+ .option('-t, --title <title>', 'New title')
592
+ .option('-u, --url <url>', 'New target URL')
593
+ .option('-r, --request <request>', 'New request')
594
+ .option('-d, --description <text>', 'New description')
595
+ .option('--cron <expression>', 'Cron expression (empty string to disable)')
596
+ .option('--no-cron', 'Disable cron (set to null)')
597
+ .option('--alert <email>', 'Failure alert email (empty string to clear)')
598
+ .option('--no-alert', 'Disable failure alert email (set to null)')
599
+ .option('--autofix', 'Enable AI Fix (auto-recovery on selector breakage)')
600
+ .option('--no-autofix', 'Disable AI Fix')
601
+ .option('-p, --params <json>', 'Runtime params as JSON array of objects (e.g. \'[{"TRAWL.paramName":"value"}]\')')
602
+ .option('--params-file <path>', 'Runtime params from a JSON file')
603
+ .option('--tier <tier>', `Force proxy tier (${VALID_TIERS.join('|')})`)
604
+ .option('--force-tier <tier>', `Raise the proxy-tier ceiling PAST the auto-cap (${VALID_TIERS.join('|')}) — history-gated: may be refused or cost more`)
605
+ .option('--json', 'Output as JSON')
606
+ .action(async (id, opts) => {
607
+ validateObjectId(id);
608
+ if (opts.tier !== undefined && !VALID_TIERS.includes(opts.tier)) {
609
+ usageError(`Invalid --tier "${opts.tier}" (allowed: ${VALID_TIERS.join(', ')})`, { json: opts.json });
610
+ return;
641
611
  }
642
- else {
643
- raw = opts.params;
612
+ if (opts.forceTier !== undefined && !VALID_TIERS.includes(opts.forceTier)) {
613
+ usageError(`Invalid --force-tier "${opts.forceTier}" (allowed: ${VALID_TIERS.join(', ')})`, { json: opts.json });
614
+ return;
644
615
  }
645
- let parsed;
646
- try {
647
- parsed = JSON.parse(raw);
616
+ const body = {};
617
+ if (opts.title !== undefined)
618
+ body.title = opts.title;
619
+ if (opts.url !== undefined)
620
+ body.url = opts.url;
621
+ if (opts.request !== undefined)
622
+ body.request = opts.request;
623
+ if (opts.description !== undefined)
624
+ body.description = opts.description;
625
+ if (opts.cron === false)
626
+ body.cron = null;
627
+ else if (typeof opts.cron === 'string')
628
+ body.cron = opts.cron === '' ? null : opts.cron;
629
+ if (opts.alert === false)
630
+ body.alert = null;
631
+ else if (typeof opts.alert === 'string')
632
+ body.alert = opts.alert === '' ? null : opts.alert;
633
+ if (opts.autofix === true)
634
+ body.autoFix = true;
635
+ else if (opts.autofix === false)
636
+ body.autoFix = false;
637
+ if (opts.params !== undefined || opts.paramsFile !== undefined) {
638
+ let raw;
639
+ if (opts.paramsFile) {
640
+ const { readFileSync } = await import('fs');
641
+ raw = readFileSync(opts.paramsFile, 'utf8');
642
+ }
643
+ else {
644
+ raw = opts.params;
645
+ }
646
+ let parsed;
647
+ try {
648
+ parsed = JSON.parse(raw);
649
+ }
650
+ catch (e) {
651
+ usageError(`Invalid JSON for --params: ${e.message}`, { json: opts.json });
652
+ return;
653
+ }
654
+ if (!Array.isArray(parsed)) {
655
+ usageError('--params must be a JSON array of objects', { json: opts.json });
656
+ return;
657
+ }
658
+ body.params = parsed;
648
659
  }
649
- catch (e) {
650
- usageError(`Invalid JSON for --params: ${e.message}`, { json: opts.json });
651
- return;
660
+ if (opts.tier !== undefined)
661
+ body.proxyTier = opts.tier;
662
+ if (opts.forceTier !== undefined) {
663
+ // Raise the ceiling; also start the run at that tier unless --tier says otherwise.
664
+ body.proxyMaxTier = opts.forceTier;
665
+ if (opts.tier === undefined)
666
+ body.proxyTier = opts.forceTier;
652
667
  }
653
- if (!Array.isArray(parsed)) {
654
- usageError('--params must be a JSON array of objects', { json: opts.json });
668
+ if (Object.keys(body).length === 0) {
669
+ if (opts.json) {
670
+ process.exitCode = reportError(new UsageError('Nothing to update. Provide at least one option.'), { json: true });
671
+ return;
672
+ }
673
+ console.log(chalk.yellow('Nothing to update. Provide at least one option.'));
655
674
  return;
656
675
  }
657
- body.params = parsed;
658
- }
659
- if (opts.tier !== undefined)
660
- body.proxyTier = opts.tier;
661
- if (opts.forceTier !== undefined) {
662
- // Raise the ceiling; also start the run at that tier unless --tier says otherwise.
663
- body.proxyMaxTier = opts.forceTier;
664
- if (opts.tier === undefined)
665
- body.proxyTier = opts.forceTier;
666
- }
667
- if (Object.keys(body).length === 0) {
676
+ const data = await spin(() => api.put(`/api/scraps/${id}`, body), {
677
+ text: 'Updating scrap…',
678
+ successText: (d) => `Scrap updated: ${chalk.bold(d._id)}`,
679
+ });
680
+ // #1559 / #86 findings 4b/5 — surface the effective tier + clamp/refuse
681
+ // reason (fixes the silent-clamp: the server may persist a lower tier
682
+ // than requested), and NEVER echo the requested value as applied when
683
+ // the server doesn't confirm it (old-server fallback below).
684
+ const tierWasRequested = opts.tier !== undefined || opts.forceTier !== undefined;
685
+ warnIfUnconfirmedTier(data, tierWasRequested, id);
686
+ const refused = Boolean(data._tierOverride?.refused);
668
687
  if (opts.json) {
669
- process.exitCode = reportError(new UsageError('Nothing to update. Provide at least one option.'), { json: true });
688
+ if (refused) {
689
+ process.exitCode = reportTierRefusal(data, true);
690
+ return;
691
+ }
692
+ json(withTierUnconfirmed(data, tierWasRequested));
670
693
  return;
671
694
  }
672
- console.log(chalk.yellow('Nothing to update. Provide at least one option.'));
673
- return;
674
- }
675
- const data = await spin(() => api.put(`/api/scraps/${id}`, body), {
676
- text: 'Updating scrap…',
677
- successText: (d) => `Scrap updated: ${chalk.bold(d._id)}`,
678
- });
679
- // #1559 / #86 findings 4b/5 — surface the effective tier + clamp/refuse
680
- // reason (fixes the silent-clamp: the server may persist a lower tier
681
- // than requested), and NEVER echo the requested value as applied when
682
- // the server doesn't confirm it (old-server fallback below).
683
- const tierWasRequested = opts.tier !== undefined || opts.forceTier !== undefined;
684
- warnIfUnconfirmedTier(data, tierWasRequested, id);
685
- const refused = Boolean(data._tierOverride?.refused);
686
- if (opts.json) {
687
- if (refused) {
688
- process.exitCode = reportTierRefusal(data, true);
689
- return;
695
+ const shown = data;
696
+ for (const key of Object.keys(body)) {
697
+ // Tier keys are rendered exclusively by renderTierOverrideHuman /
698
+ // warnIfUnconfirmedTier above never echo them here, whether or not
699
+ // _tierOverride came back (an old-server echo of the REQUESTED value
700
+ // is exactly the silent-clamp lie #1559 fixed).
701
+ if (key === 'proxyTier' || key === 'proxyMaxTier')
702
+ continue;
703
+ const src = key in shown ? shown[key] : body[key];
704
+ console.log(chalk.dim(` ${key}: `) + String(src ?? '—'));
690
705
  }
691
- json(withTierUnconfirmed(data, tierWasRequested));
692
- return;
693
- }
694
- const shown = data;
695
- for (const key of Object.keys(body)) {
696
- // Tier keys are rendered exclusively by renderTierOverrideHuman /
697
- // warnIfUnconfirmedTier above — never echo them here, whether or not
698
- // _tierOverride came back (an old-server echo of the REQUESTED value
699
- // is exactly the silent-clamp lie #1559 fixed).
700
- if (key === 'proxyTier' || key === 'proxyMaxTier')
701
- continue;
702
- const src = key in shown ? shown[key] : body[key];
703
- console.log(chalk.dim(` ${key}: `) + String(src ?? '—'));
704
- }
705
- renderTierOverrideHuman(data);
706
- if (refused)
707
- process.exitCode = 1;
708
- });
706
+ renderTierOverrideHuman(data);
707
+ if (refused)
708
+ process.exitCode = 1;
709
+ });
710
+ }
711
+ attachUpdateCommand(scraps, { hidden: true });
709
712
  // run — promoted to a top-level verb (#108)
710
713
  export function attachRunCommand(parent, attachOpts = {}) {
711
714
  return parent
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ 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, attachListCommand, attachGetCommand, attachRunCommand, attachDataCommand, attachHistoryCommand, attachRunInfoCommand, attachTriggerCommand, } from './commands/scraps.js';
7
+ import { scraps, attachListCommand, attachGetCommand, attachRunCommand, attachDataCommand, attachHistoryCommand, attachRunInfoCommand, attachTriggerCommand, attachUpdateCommand, } 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';
@@ -108,11 +108,14 @@ export function createProgram() {
108
108
  attachHistoryCommand(program);
109
109
  attachRunInfoCommand(program);
110
110
  attachTriggerCommand(program);
111
+ attachUpdateCommand(program);
111
112
  program.addCommand(whoami);
112
113
  program.addCommand(ping);
113
114
  // 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.
115
+ // holds every pre-#108 management command (create/delete/banner/
116
+ // watch/account.*/session.*/doctor/autofix/snapshot) exactly as before
117
+ // `update` is now a promoted core verb (#120), still reachable (hidden)
118
+ // under `scraps` for backward compatibility.
116
119
  program.commandsGroup(MANAGEMENT_GROUP);
117
120
  program.addCommand(scraps);
118
121
  program.addCommand(skills);
@@ -20,7 +20,7 @@ agent should never need them.)
20
20
 
21
21
  ## Core commands (agent + human)
22
22
 
23
- These ten commands are the CLI's agent+human surface — `--json` is
23
+ These eleven commands are the CLI's agent+human surface — `--json` is
24
24
  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
 
@@ -34,6 +34,8 @@ trawl data <id> [--json] [--fresh] [--errors] Get scrap data (last persisted ru
34
34
  trawl history <id> [--json] [-n <limit>] List past runs for a scrap
35
35
  trawl run-info <hid> [--json] Show details of a single run
36
36
  trawl trigger <id> [--watch] [--wait] [--json] Launch a scrap as a background worker
37
+ trawl 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 <t>] [--force-tier <t>] [--json]
38
+ Update an existing scrap
37
39
  trawl whoami [--json] Show the authenticated user's identity
38
40
  trawl ping [--json] Health/version handshake against the Trawl API
39
41
  ```
@@ -59,7 +61,7 @@ default.
59
61
  > scheduled to re-run every day at 07:00 UTC by default
60
62
  > (`cron: "0 7 * * *"`, unrelated to `--no-autofix`) — each recurring run
61
63
  > consumes execute quota. Disable or change it once you've reviewed the
62
- > scrap: `trawl scraps update <id> --no-cron` (or `--cron <expr>`). Finally,
64
+ > scrap: `trawl update <id> --no-cron` (or `--cron <expr>`). Finally,
63
65
  > if `TRAWL_TIMEOUT` is set globally for a tighter budget than 300s, it
64
66
  > clamps `create`'s ceiling too (env always wins) — unset it or raise it
65
67
  > before calling `create`.
@@ -69,9 +71,10 @@ default.
69
71
  > a hidden alias. Prefer the bare top-level form above; it's what
70
72
  > `trawl --help` now shows.
71
73
 
72
- For the full flag reference (tier overrides on `scraps create`/`scraps
73
- update` — the core `create` verb above has no `--tier` of its own, the
74
- `--watch` polling mechanics, retention/regression semantics on `data`, …)
74
+ For the full flag reference (tier overrides on `scraps create` (management)
75
+ and the core `update` verb above note the core `create` verb has no
76
+ `--tier` of its own, unlike `scraps create`, the `--watch` polling mechanics,
77
+ retention/regression semantics on `data`, …)
75
78
  see the README's [Core commands](../README.md#core-commands-agent--human) section
76
79
  — this doc intentionally stays minimal.
77
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trawlme/cli",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Trawl CLI — manage scraps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {