@trawlme/cli 3.12.0 → 3.12.2

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.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/create.d.ts +0 -28
  3. package/dist/commands/create.js +0 -89
  4. package/dist/commands/doctor.d.ts +0 -79
  5. package/dist/commands/doctor.js +1 -187
  6. package/dist/commands/login.js +0 -67
  7. package/dist/commands/ping.d.ts +0 -15
  8. package/dist/commands/ping.js +0 -15
  9. package/dist/commands/scraps.d.ts +0 -120
  10. package/dist/commands/scraps.js +10 -724
  11. package/dist/commands/skills.js +0 -22
  12. package/dist/commands/spec.d.ts +0 -85
  13. package/dist/commands/spec.js +0 -67
  14. package/dist/commands/telemetry.js +0 -4
  15. package/dist/commands/token.js +0 -28
  16. package/dist/commands/upgrade.js +0 -22
  17. package/dist/commands/whoami.d.ts +0 -12
  18. package/dist/commands/whoami.js +0 -6
  19. package/dist/index.d.ts +0 -188
  20. package/dist/index.js +0 -349
  21. package/dist/lib/api.d.ts +0 -78
  22. package/dist/lib/api.js +1 -320
  23. package/dist/lib/cdp-pipe.d.ts +0 -72
  24. package/dist/lib/cdp-pipe.js +1 -81
  25. package/dist/lib/chrome-discovery.d.ts +0 -11
  26. package/dist/lib/chrome-discovery.js +0 -19
  27. package/dist/lib/chrome-launch.d.ts +0 -40
  28. package/dist/lib/chrome-launch.js +0 -69
  29. package/dist/lib/config.d.ts +0 -53
  30. package/dist/lib/config.js +0 -55
  31. package/dist/lib/confirm.d.ts +0 -55
  32. package/dist/lib/confirm.js +0 -47
  33. package/dist/lib/docs.d.ts +0 -123
  34. package/dist/lib/docs.js +0 -169
  35. package/dist/lib/errors.d.ts +0 -134
  36. package/dist/lib/errors.js +0 -151
  37. package/dist/lib/format.d.ts +0 -6
  38. package/dist/lib/format.js +0 -6
  39. package/dist/lib/json.d.ts +0 -35
  40. package/dist/lib/json.js +0 -48
  41. package/dist/lib/jwt.d.ts +0 -7
  42. package/dist/lib/jwt.js +0 -7
  43. package/dist/lib/pinch.d.ts +0 -53
  44. package/dist/lib/pinch.js +6 -112
  45. package/dist/lib/pinchAnimation.d.ts +0 -16
  46. package/dist/lib/pinchAnimation.js +8 -29
  47. package/dist/lib/posthog.d.ts +0 -9
  48. package/dist/lib/posthog.js +0 -23
  49. package/dist/lib/prompt.js +1 -20
  50. package/dist/lib/secure-transport.d.ts +0 -7
  51. package/dist/lib/secure-transport.js +0 -24
  52. package/dist/lib/session-capture-guard.d.ts +0 -15
  53. package/dist/lib/session-capture-guard.js +0 -5
  54. package/dist/lib/session-capture.d.ts +0 -125
  55. package/dist/lib/session-capture.js +0 -281
  56. package/dist/lib/skills.d.ts +0 -175
  57. package/dist/lib/skills.js +1 -216
  58. package/dist/lib/skillsNudge.d.ts +0 -17
  59. package/dist/lib/skillsNudge.js +0 -83
  60. package/dist/lib/spinner.d.ts +0 -39
  61. package/dist/lib/spinner.js +0 -40
  62. package/dist/lib/storage-state.d.ts +0 -112
  63. package/dist/lib/storage-state.js +0 -131
  64. package/dist/lib/tips.d.ts +0 -38
  65. package/dist/lib/tips.js +0 -77
  66. package/dist/lib/updateCheckWorker.js +0 -14
  67. package/dist/lib/updateNotifier.d.ts +0 -17
  68. package/dist/lib/updateNotifier.js +0 -53
  69. package/dist/lib/validate.d.ts +0 -8
  70. package/dist/lib/validate.js +0 -8
  71. package/dist/lib/version.d.ts +0 -12
  72. package/dist/lib/version.js +1 -13
  73. package/docs/agent-quickstart.md +2 -2
  74. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -24,30 +24,6 @@ import { getApiUrl } from './lib/config.js';
24
24
  import { resolveDocsUrls, docsFooterLine } from './lib/docs.js';
25
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
26
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
27
- /**
28
- * Derive a safe telemetry event name from a *resolved* commander Command —
29
- * NEVER from raw argv. Flag values (e.g. the string after --password/--email/
30
- * --url) don't start with '-' and would otherwise survive an argv filter and
31
- * leak to PostHog (#67). Falls back to 'unknown' when no command resolved
32
- * (e.g. an error thrown before any action ran).
33
- *
34
- * #88 item 9 — walks the FULL parent chain, not just the immediate parent:
35
- * the old one-level join (`${parent.name()} ${own.name()}`) resolved a
36
- * 4-deep command like `scraps account session set` down to just "session
37
- * set", silently dropping "scraps account". The root program node (the
38
- * 'trawl' Command itself, which has no `.parent`) is excluded from the
39
- * chain — matching the pre-existing convention that a direct child of the
40
- * root (e.g. `scraps list`, `telemetry on`) is named relative to its
41
- * immediate group, never prefixed with the program name.
42
- *
43
- * #108 note: promoting a verb to a top-level command (see `createProgram`
44
- * below) renamed ITS resolved telemetry name from `scraps <verb>` to
45
- * `<verb>` — the canonical top-level attach and the legacy hidden
46
- * `scraps <verb>` attach are two separate Command instances (scraps.ts's
47
- * double-attach factories), each with its own parent chain, so they
48
- * resolve to two different names here even though they run the same
49
- * handler. Intentional (the canonical command IS now `<verb>`), not a bug.
50
- */
51
27
  export function resolveCommandName(actionCommand) {
52
28
  if (!actionCommand)
53
29
  return 'unknown';
@@ -57,16 +33,8 @@ export function resolveCommandName(actionCommand) {
57
33
  chain.unshift(current.name());
58
34
  current = current.parent;
59
35
  }
60
- // `current` is now the root (no parent) — its name is excluded by design.
61
- // If the chain came up empty, actionCommand itself IS the root (no parent
62
- // at all) — fall back to its own name so the function never returns ''.
63
36
  return chain.length > 0 ? chain.join(' ') : actionCommand.name();
64
37
  }
65
- /**
66
- * Walk the full command tree and produce every valid resolveCommandName()
67
- * token — the allowlist registered with posthog.ts so captureCommand can
68
- * never be handed a free-form string.
69
- */
70
38
  export function collectCommandNames(root) {
71
39
  const names = [];
72
40
  const walk = (cmd) => {
@@ -78,16 +46,6 @@ export function collectCommandNames(root) {
78
46
  walk(root);
79
47
  return names;
80
48
  }
81
- /**
82
- * #108 — surface reorg into two `trawl --help` tiers. Core verbs are
83
- * agent+human, `--json` first-class, non-interactive; Management is the
84
- * existing human/CI surface, kept but grouped so top-level help reads
85
- * simple. Commander v14's native per-command help group (`.commandsGroup()`
86
- * sets the default a subsequently-registered command inherits via
87
- * `.helpGroup()`) drives the section headings — group ORDER in the printed
88
- * help follows first-seen insertion order into `program.commands`, so every
89
- * Core command is registered below before any Management one.
90
- */
91
49
  const CORE_GROUP = 'Core commands (agent + human):';
92
50
  const MANAGEMENT_GROUP = 'Management commands (human/CI):';
93
51
  export function createProgram() {
@@ -96,28 +54,6 @@ export function createProgram() {
96
54
  .description('Trawl CLI — manage scraps from the terminal')
97
55
  .version(pkg.version)
98
56
  .option('--debug', 'Show full error stack traces');
99
- // Core verbs (#108) — promoted/listed first, registration order now
100
- // follows the guided flow a new user (or agent) actually walks: create it
101
- // → list/get to see it → run/trigger to execute it → data to read the
102
- // result → history/run-info for deeper diagnostics → whoami/token/ping as
103
- // standalone identity/credential/health utilities (#149 item 6 — the
104
- // pre-#149 order had `run` wedged between `create` and `list`, and
105
- // `trigger` stranded after `run-info`, both ahead of ever having listed or
106
- // inspected anything). `list`/`get`/`run`/`data`/`history`/`run-info`/
107
- // `trigger` are built via scraps.ts's exported attachXCommand() factories —
108
- // the SAME definition also stays wired (hidden) under `scraps` there, so
109
- // every pre-#108 `trawl scraps <verb>` invocation keeps resolving (no
110
- // breaking change).
111
- //
112
- // #114 — `create` replaced `fetch` in this slot: `POST /api/ai/wizard`
113
- // (AI-generate + persist + first-run + autofix), a distinct command from
114
- // the still-untouched `trawl scraps create` (raw-script management verb).
115
- //
116
- // #149 item 5 — `token` promoted from Management into Core: it satisfies
117
- // every criterion the CORE_GROUP heading itself claims (agent + human,
118
- // `--json` first-class, never interactive) at least as well as `whoami`/
119
- // `ping` do, and exists specifically to feed an agent's MCP Bearer auth
120
- // header (`$(trawl token)`) — a Management/human-CI bucket undersells that.
121
57
  program.commandsGroup(CORE_GROUP);
122
58
  program.addCommand(create);
123
59
  attachListCommand(program);
@@ -130,13 +66,7 @@ export function createProgram() {
130
66
  program.addCommand(whoami);
131
67
  program.addCommand(token);
132
68
  program.addCommand(ping);
133
- // #170 — `spec` last: it describes the whole tree above it, so it reads
134
- // naturally as the final "and here's the full machine-readable map" entry
135
- // rather than being wedged between identity/credential/health utilities.
136
69
  program.addCommand(spec);
137
- // Management (#108) — human/CI surface, grouped below. `scraps` still
138
- // holds every pre-#108 management command (create/update/delete/banner/
139
- // watch/account.*/session.*/doctor/autofix/snapshot) exactly as before.
140
70
  program.commandsGroup(MANAGEMENT_GROUP);
141
71
  program.addCommand(scraps);
142
72
  program.addCommand(skills);
@@ -144,28 +74,9 @@ export function createProgram() {
144
74
  program.addCommand(logout);
145
75
  program.addCommand(telemetry);
146
76
  program.addCommand(upgrade);
147
- // #185 — one dim line pointing at the docs, appended after EVERY `--help`
148
- // in the tree (registering 'afterAll' on the root fires for a subcommand's
149
- // own `--help` too — commander emits the afterAllHelp event on every
150
- // ancestor of whichever command's outputHelp() ran, root included).
151
- // Gated the way lib/tips.ts gates its own footer-shaped output: a pure,
152
- // synchronous helper (docsFooterText, exported for unit testing) wrapped
153
- // in try/catch so a broken footer can never turn a clean `--help` into a
154
- // crash — but deliberately WITHOUT tips.ts's TTY/--json gate: `--help`
155
- // itself never emits a --json payload (nothing here can pollute that
156
- // channel), and a docs line inside `--help | less` is exactly the kind of
157
- // thing worth keeping, unlike a promotional nudge.
158
77
  program.addHelpText('afterAll', docsFooterText);
159
78
  return program;
160
79
  }
161
- /**
162
- * #185 — see the `addHelpText('afterAll', …)` call above for why this is
163
- * unconditional (no TTY/--json gate) and why a thrown error must never
164
- * escape: commander calls this synchronously while already writing help
165
- * output, so a throw here would crash a `--help` invocation, the one code
166
- * path this whole feature is not allowed to touch (see docs.ts's own
167
- * `docsFooterLine` for the "never a guessed URL" contract this composes).
168
- */
169
80
  export function docsFooterText() {
170
81
  try {
171
82
  const line = docsFooterLine(resolveDocsUrls({ apiBaseUrl: getApiUrl() }).docsUrl);
@@ -175,18 +86,6 @@ export function docsFooterText() {
175
86
  return '';
176
87
  }
177
88
  }
178
- /**
179
- * True when this module is the process entrypoint (not merely imported by a test).
180
- *
181
- * npm installs the global bin as a SYMLINK (`bin/trawl` -> `dist/index.js`). For an
182
- * ESM entrypoint Node realpaths the module URL (`import.meta.url` = the real
183
- * `dist/index.js`) but leaves `process.argv[1]` as the symlink path — so a raw
184
- * `moduleUrl === pathToFileURL(argv1).href` compare is FALSE for the normal global
185
- * invocation, the guard never fires, and `runCli()` never runs (silent no-op, #103).
186
- * Canonicalise both sides with `realpathSync` before comparing. A genuine entrypoint
187
- * was just loaded by Node, so both realpath calls resolve; the catch only trips for a
188
- * non-file module URL (exotic loaders) — correctly "not the entrypoint".
189
- */
190
89
  export function isEntryPoint(argv1, moduleUrl) {
191
90
  if (argv1 === undefined)
192
91
  return false;
@@ -197,15 +96,6 @@ export function isEntryPoint(argv1, moduleUrl) {
197
96
  return false;
198
97
  }
199
98
  }
200
- /**
201
- * True when the invocation is a pure `--help`/`--version` query, a bare
202
- * `trawl` with no subcommand (commander prints top-level help and exits), or
203
- * `trawl help [command]`. None of these should trigger the skills auto-sync
204
- * (a filesystem-mutating startup side effect) — a user running `trawl
205
- * --version` (or just `trawl`) never expects it to rewrite their skills
206
- * dirs. (#73, extended #86 finding 7 for the bare-invocation + `help`
207
- * subcommand cases)
208
- */
209
99
  export function isHelpOrVersion(argv) {
210
100
  if (argv.some((a) => a === '-h' || a === '--help' || a === '-V' || a === '--version'))
211
101
  return true;
@@ -216,76 +106,9 @@ export function isHelpOrVersion(argv) {
216
106
  return true;
217
107
  return false;
218
108
  }
219
- /**
220
- * True for a bare `trawl` invocation — no subcommand, no flags at all. This
221
- * is the CLI's "first thing a new user sees" moment (commander prints its
222
- * own top-level help right after), distinct from `isHelpOrVersion` which is
223
- * intentionally broader (also matches `--help`/`--version`/`trawl help`
224
- * anywhere in argv) — the Pinch wave banner (#94) only wants the narrowest
225
- * case so it never shows up ahead of e.g. `trawl scraps --help`.
226
- */
227
109
  export function isBareInvocation(argv) {
228
110
  return argv.slice(2).length === 0;
229
111
  }
230
- /**
231
- * True for `trawl spec` (with or without `--json`) — deliberately NOT folded
232
- * into `isHelpOrVersion` above: that predicate's name and doc comment are
233
- * about help/version queries specifically, and `spec` is a real, data-
234
- * bearing command (see `isHelpOrVersion`'s own #170 test case), not a help
235
- * query. This is its own narrow predicate for a DIFFERENT reason: `spec
236
- * --json` is documented (docs/agent-quickstart.md) as the first call an AI
237
- * agent makes against this CLI, so it must not mutate anything the caller
238
- * did not ask it to — same requirement `--help`/`--version` already got
239
- * from `isHelpOrVersion` (#73), extended here to cover `spec` too.
240
- *
241
- * Scope, stated honestly rather than aspirationally: this guard suppresses
242
- * the skills auto-sync (an `rmSync(recursive)` + `cpSync` over the user's
243
- * `~/.claude/skills`) and the update notifier (a config write plus a
244
- * DETACHED CHILD that queries the npm registry — fatal in an
245
- * egress-restricted sandbox). It does NOT suppress `initPostHog()`, which
246
- * still creates the `conf` config file and mints a persistent
247
- * `telemetryUserId` on first run, exactly as it does for every other
248
- * command. That one is deliberate: `spec` is the signal that tells us
249
- * whether agents are actually adopting the CLI, so it stays measured, and
250
- * the caller's own controls (`TRAWL_TELEMETRY=0`, `DO_NOT_TRACK=1`) are the
251
- * opt-out. That same first run also has `initPostHog()` write a one-time
252
- * telemetry disclosure line (`ℹ Trawl CLI collects anonymous usage
253
- * telemetry…`) to STDERR, ahead of the JSON payload — this guard does not
254
- * suppress that either. `stdout` stays pure JSON either way, so this only
255
- * bites a caller that merges the two streams (e.g. `2>&1`); whether a `spec`
256
- * probe specifically should suppress the notice is a product call,
257
- * deliberately not taken here. Do not upgrade this paragraph back to "no
258
- * filesystem side effect" without also gating telemetry — a comment that
259
- * overstates its own invariant is worse than no comment, because the next
260
- * reader trusts it.
261
- *
262
- * #185 — `spec --json` (not plain `spec`) now DOES make its own bounded
263
- * (2s), swallowed-on-failure GET for `externalDocs.url` (see spec.ts's
264
- * `fetchExternalDocsUrl`) — this is not a regression of the "no mutation"
265
- * invariant above (a GET mutates nothing, and any failure — including no
266
- * egress at all — degrades to the sync host-derivation fallback, never a
267
- * thrown error), just a second, narrower kind of side effect this guard was
268
- * never meant to suppress in the first place.
269
- *
270
- * Before this, `trawl spec --json` silently re-synced
271
- * `~/.claude/skills` (and `./.claude/skills`) and printed `trawl: re-synced
272
- * skill …` lines to stderr BEFORE the JSON payload — an agent's very first
273
- * probe of the CLI mutated the user's filesystem and polluted the channel
274
- * it was about to parse.
275
- *
276
- * Matches `spec` as the FIRST positional token, skipping any leading
277
- * `-`-prefixed tokens first (a global flag ahead of the subcommand, e.g.
278
- * `trawl --debug spec --json`) — the same argv-scan discipline `hasJsonFlag`
279
- * below already applies. #170 review F4 — before this, a bare
280
- * `argv.slice(2)[0] === 'spec'` check was defeated by ANY leading flag:
281
- * `trawl --debug spec --json` read `--debug` as the first positional, missed
282
- * the match entirely, and fell through to the normal startup path — running
283
- * the filesystem-mutating skills auto-sync during what's documented
284
- * (docs/agent-quickstart.md) as an agent's read-only first probe of the CLI.
285
- * Still never a bare substring search: e.g. `trawl scraps create --title
286
- * spec` (the literal string "spec" as a FLAG VALUE, not a leading flag) never
287
- * matches, since `scraps` — the first non-`-`-prefixed token — isn't `spec`.
288
- */
289
112
  export function isSpecQuery(argv) {
290
113
  const args = argv.slice(2);
291
114
  let i = 0;
@@ -293,62 +116,16 @@ export function isSpecQuery(argv) {
293
116
  i++;
294
117
  return args[i] === 'spec';
295
118
  }
296
- /** Best-effort scan for a `--json` flag in raw argv, used only when parsing
297
- * itself failed before any command's own `.opts()` could be resolved (a
298
- * commander usage error — unknown option/command, missing required arg). Same
299
- * "argv scan, never trust flag values" caveat as isHelpOrVersion: positional
300
- * values are never mistaken for `--json` since they don't equal the literal
301
- * string. (#86 finding 3)
302
- *
303
- * #88 item 5 — only scans tokens BEFORE the first bare `--`. Commander treats
304
- * `--` as "end of options": everything after it is a positional operand, not
305
- * a flag, even if the literal text is `--json`. `scraps list -- --json`
306
- * passes `--json` as an (excess) positional argument, not the flag — an
307
- * unscoped `argv.includes('--json')` would still match it and wrongly emit a
308
- * JSON envelope for what is actually a plain usage error with no --json
309
- * requested at all.
310
- */
311
119
  export function hasJsonFlag(argv) {
312
120
  const dashDashIdx = argv.indexOf('--');
313
121
  const scanned = dashDashIdx === -1 ? argv : argv.slice(0, dashDashIdx);
314
122
  return scanned.includes('--json');
315
123
  }
316
- /**
317
- * Commander's default (no exitOverride) calls `process.exit()` directly for
318
- * a usage error (unknown option/command, missing required arg) or a
319
- * --help/--version/`help` query — bypassing runCli's try/catch/finally
320
- * entirely, so the telemetry shutdown() flush below never runs and a usage
321
- * error exits 1 (the generic bug bucket) instead of its own distinct code.
322
- * `program.exitOverride()` on the root command alone does NOT fix this for
323
- * subcommands added via `addCommand()` (login/scraps/skills/telemetry/token
324
- * are each built as standalone Command instances in their own module and
325
- * only ever copy inherited settings — including exitOverride — from a parent
326
- * at `.command()` construction time, which for these root-level modules never
327
- * happens). Every node in the tree needs its own exitOverride() call, so this
328
- * walks the whole tree and installs it everywhere. (#86 finding 3)
329
- */
330
124
  export function applyExitOverride(cmd) {
331
125
  cmd.exitOverride();
332
126
  for (const sub of cmd.commands)
333
127
  applyExitOverride(sub);
334
128
  }
335
- /**
336
- * #149 item 1 — commander's own usage-error line (unknown option, missing
337
- * required arg, unknown command, …) is written via `Command#error()` ->
338
- * `outputError()` straight to `process.stderr` BEFORE the exitOverride throw
339
- * below is ever caught — in commander's own plain, uncolored `"error: …"`
340
- * format, a visibly different convention from every other error this CLI
341
- * prints (`reportError`'s `chalk.red('✗ ' + message)`). `configureOutput`'s
342
- * `outputError` hook is exactly the one commander documents for reformatting
343
- * the error text itself (`writeErr` also carries unrelated help output, e.g.
344
- * `showHelpAfterError` — never enabled in this CLI, but out of scope to
345
- * touch here). Overriding it on every node in the tree (same walk shape as
346
- * applyExitOverride, for the same reason: addCommand()-attached subtrees
347
- * don't otherwise inherit a parent's configureOutput) reformats that one line
348
- * through the same red ✗ convention, stripping the redundant "error: " prefix
349
- * first (stripCommanderErrorPrefix — the same prefix also leaks verbatim
350
- * into the --json envelope's `message` field, see runCli's catch below).
351
- */
352
129
  export function applyOutputConfiguration(cmd) {
353
130
  cmd.configureOutput({
354
131
  outputError: (str, write) => {
@@ -359,27 +136,7 @@ export function applyOutputConfiguration(cmd) {
359
136
  for (const sub of cmd.commands)
360
137
  applyOutputConfiguration(sub);
361
138
  }
362
- /** Commander's own codes for a successful --help/--version/`help` query —
363
- * these already printed their own output (to stdout) via commander itself;
364
- * runCli's catch must treat them as a clean exit, not an error. (#86 finding 3) */
365
139
  const HELP_OR_VERSION_CODES = new Set(['commander.helpDisplayed', 'commander.help', 'commander.version']);
366
- /**
367
- * #88 item 6 — best-effort command-name recovery for a commander parse error
368
- * that happened BEFORE any action ran, so `currentCommand` (the preAction
369
- * hook's resolved command) is still undefined — e.g. an unknown option on an
370
- * otherwise-valid subcommand, or excess arguments. Without this, EVERY such
371
- * failure previously collapsed to resolveCommandName(undefined) === 'unknown'
372
- * — and since 'unknown' was never itself allowlisted at the
373
- * registerAllowedCommands call site (see runCli below), that capture was
374
- * silently dropped by captureCommand's allowlist check: a dead branch that
375
- * looked like it reported telemetry but never actually did.
376
- *
377
- * Matches ONLY a name already present in `allowedNames` (the real command
378
- * tree, from collectCommandNames) — never invents one from raw argv text.
379
- * Checks the two-token form first (`scraps boom`) since most usage errors
380
- * happen on a nested leaf command; falls back to the single top-level token,
381
- * then to 'unknown' (now itself allowlisted, so that capture fires too).
382
- */
383
140
  export function bestEffortCommandName(argv, allowedNames) {
384
141
  const a2 = argv[2];
385
142
  const a3 = argv[3];
@@ -395,31 +152,16 @@ export function bestEffortCommandName(argv, allowedNames) {
395
152
  export async function runCli(argv = process.argv) {
396
153
  if (!isHelpOrVersion(argv) && !isSpecQuery(argv))
397
154
  autoUpdateInstalledSkills();
398
- // Pinch wave banner (#94) — bare `trawl` only, guarded so it never shows
399
- // under NO_COLOR/non-TTY/piped output (pinchEnabled() covers all three).
400
155
  if (isBareInvocation(argv) && pinchEnabled()) {
401
156
  console.log(renderPinch('wave'));
402
157
  console.log();
403
158
  }
404
159
  initPostHog();
405
160
  const program = createProgram();
406
- // Must run before parseAsync — installs on every node in the tree,
407
- // including subcommands added via addCommand() that don't otherwise
408
- // inherit it. (#86 finding 3)
409
161
  applyExitOverride(program);
410
- // Same tree-walk requirement as applyExitOverride, same reason — installs
411
- // the unified red ✗ error formatting on every node. (#149 item 1)
412
162
  applyOutputConfiguration(program);
413
163
  const commandNames = collectCommandNames(program);
414
- // #88 item 6 — 'unknown' is a legitimate resolveCommandName() output (the
415
- // fallback for "no command resolved at all"), not free-form user input —
416
- // it must be explicitly allowlisted here or every capture that falls back
417
- // to it is silently dropped by captureCommand's allowlist check (a dead
418
- // branch that looks like it reports telemetry but never does).
419
164
  registerAllowedCommands([...commandNames, 'unknown']);
420
- // Track start times + the currently-resolved command per instance, so the
421
- // catch handler below can derive the exact same safe name the success path
422
- // uses — it must never re-derive anything from argv.
423
165
  const startTimes = new WeakMap();
424
166
  let currentCommand;
425
167
  program.hook('preAction', (_thisCommand, actionCommand) => {
@@ -429,8 +171,6 @@ export async function runCli(argv = process.argv) {
429
171
  program.hook('postAction', (_thisCommand, actionCommand) => {
430
172
  const start = startTimes.get(actionCommand);
431
173
  if (start !== undefined) {
432
- // Actions can fail without throwing (process.exitCode set directly) —
433
- // report the real outcome instead of hardcoding success.
434
174
  const exitCode = typeof process.exitCode === 'number' ? process.exitCode : 0;
435
175
  void captureCommand(resolveCommandName(actionCommand), {
436
176
  duration_ms: Date.now() - start,
@@ -439,22 +179,6 @@ export async function runCli(argv = process.argv) {
439
179
  }
440
180
  });
441
181
  try {
442
- // #148 — a bare `trawl` (no args at all) reads as "let me look around",
443
- // not a failure: it should behave exactly like `trawl --help` (same
444
- // text, stdout, exit 0). Left to commander's own default, a program with
445
- // subcommands and no root action handler treats zero args as "probably
446
- // missing subcommand" and calls `this.help({ error: true })` internally
447
- // (see node_modules/commander/lib/command.js `_parseCommand`) — which
448
- // writes the SAME help text to stderr and throws a CommanderError with
449
- // exitCode 1 (caught below, bucketed into HELP_OR_VERSION_CODES since its
450
- // code is 'commander.help', so only the exit code carries the mistake
451
- // through — text already went to the wrong stream by then). Intercepting
452
- // here, before parseAsync ever runs, avoids fighting that internal
453
- // decision entirely and can't affect a genuine unknown top-level command
454
- // (`trawl frobnicate`) — that path only runs when `isBareInvocation` is
455
- // false, so it still reaches `this.unknownCommand()` unchanged (stderr,
456
- // exit 2, code 'commander.unknownCommand' — never in
457
- // HELP_OR_VERSION_CODES).
458
182
  if (isBareInvocation(argv)) {
459
183
  program.outputHelp();
460
184
  process.exitCode = 0;
@@ -467,40 +191,13 @@ export async function runCli(argv = process.argv) {
467
191
  const { debug } = program.opts();
468
192
  const isDebug = Boolean(debug || process.env['DEBUG']);
469
193
  if (err instanceof CommanderError) {
470
- // Commander's own parse-time errors (exitOverride, #86 finding 3) —
471
- // a distinct family from our ApiError/NetworkError/UsageError/generic
472
- // Error taxonomy, so classifyError/reportError don't apply here.
473
194
  if (HELP_OR_VERSION_CODES.has(err.code)) {
474
- // --help / --version / `trawl help` already printed their own
475
- // output via commander itself — nothing else to print, just adopt
476
- // commander's suggested exit code (0) and fall through to the
477
- // shared shutdown() flush below.
478
195
  process.exitCode = err.exitCode;
479
196
  }
480
197
  else {
481
- // A usage error (unknown option/command, missing required arg, …) —
482
- // commander already wrote its own human-readable line to stderr via
483
- // Command#error() (now reformatted through the same red ✗ convention
484
- // by applyOutputConfiguration, #149 item 1), so this never duplicates
485
- // it. Force exit code 2 (usage) regardless of whichever code
486
- // commander suggests (it defaults these to 1), and add the --json
487
- // machine envelope when resolvable — parsing failed before any
488
- // command's own --json flag could be read off `currentCommand`
489
- // (preAction never fired), so scan raw argv instead. (#86 finding 3)
490
198
  if (isDebug)
491
199
  console.error(err);
492
200
  if (hasJsonFlag(argv)) {
493
- // #149 item 2 — err.message still carries commander's own literal
494
- // "error: " prefix (set on the CommanderError itself, independent
495
- // of the outputError override above, which only reformats what
496
- // gets WRITTEN to stderr) — strip it so the envelope's `message`
497
- // stays clean for a machine parser.
498
- //
499
- // #170 review F3 — typed as ErrorEnvelope (not a bare object
500
- // literal) so the compiler enforces `retryable` being non-optional
501
- // here too. Mutation-proven: `tsc --noEmit` stayed at exit 0 with
502
- // `...retryFieldsFor('usage')` removed entirely before this
503
- // annotation existed.
504
201
  const envelope = {
505
202
  message: stripCommanderErrorPrefix(err.message),
506
203
  kind: 'usage',
@@ -509,11 +206,6 @@ export async function runCli(argv = process.argv) {
509
206
  console.log(JSON.stringify({ error: envelope }));
510
207
  }
511
208
  process.exitCode = 2;
512
- // #88 item 6 — currentCommand is still undefined here whenever the
513
- // parse error happened before preAction fired (the common case for a
514
- // usage error — commander validates flags/arity before invoking the
515
- // action). Try a best-effort match against the real command tree
516
- // instead of collapsing straight to 'unknown'.
517
209
  const commandName = currentCommand
518
210
  ? resolveCommandName(currentCommand)
519
211
  : bestEffortCommandName(argv, commandNames);
@@ -521,73 +213,33 @@ export async function runCli(argv = process.argv) {
521
213
  }
522
214
  }
523
215
  else {
524
- // Map the error to a distinct exit code + machine envelope instead of a
525
- // uniform 1 — agents driving this CLI unattended need to tell
526
- // auth-expired (3) from not-found (4) from network-down (5) from a bad
527
- // flag (2) apart from an arbitrary bug (1). (#71)
528
216
  const { exitCode } = classifyError(err);
529
- // Capture error telemetry from the resolved command only — never argv.
530
217
  void captureCommand(resolveCommandName(currentCommand), {
531
218
  exit_code: exitCode,
532
219
  error: err.name,
533
220
  });
534
- // A --json subcommand must keep stdout pure JSON even on failure — read
535
- // the resolved command's own --json flag (never argv) so the error
536
- // envelope lands on the same channel the success path would have used.
537
221
  const wantsJson = Boolean(currentCommand?.opts()?.json);
538
222
  if (isDebug)
539
223
  console.error(err);
540
- // reportError is the single formatting path (#86 finding 9) — prints
541
- // EITHER the --json envelope (stdout) OR the human "✗ message" line
542
- // (stderr), never both; `quiet` skips the human line when the raw
543
- // stack was already dumped above under --debug.
544
224
  process.exitCode = reportError(err, { json: wantsJson, quiet: isDebug });
545
- // Pinch confused frame (#94) — never under --json (stdout must stay
546
- // pure JSON) and guarded by pinchEnabled() (NO_COLOR/non-TTY/piped).
547
- // Printed to stderr, alongside reportError's own human line above.
548
225
  if (!wantsJson && pinchEnabled()) {
549
226
  console.error(renderPinch('confused'));
550
227
  }
551
228
  }
552
229
  }
553
230
  finally {
554
- // Flush + close telemetry before the process exits. A `process.on('exit')`
555
- // handler cannot reliably run async work, so this must happen here.
556
231
  await shutdown();
557
- // Passive "update available" notifier (#129) — never on a help/version
558
- // query, and each half individually guarded so a notifier bug can never
559
- // turn a successful command into a failure or change its exit code.
560
- // #170 review F5 — never on a `spec` query either: `maybeNotifyUpdate`/
561
- // `scheduleUpdateCheck` write to disk (`Library/Preferences/…/config.json`,
562
- // `update-check.json`) and the latter spawns a DETACHED child that queries
563
- // the npm registry — real filesystem + network side effects `isSpecQuery`'s
564
- // own doc comment already promises `spec --json` never has. Before this,
565
- // `spec`'s only protection was skipping the skills auto-sync above; this
566
- // finally block ran unconditionally regardless.
567
232
  if (!isHelpOrVersion(argv) && !isSpecQuery(argv)) {
568
233
  try {
569
234
  maybeNotifyUpdate();
570
235
  }
571
236
  catch {
572
- // swallow — see updateNotifier.ts, this is already self-guarded too.
573
237
  }
574
238
  try {
575
239
  scheduleUpdateCheck();
576
240
  }
577
241
  catch {
578
- // swallow — background refresh must never affect this invocation.
579
242
  }
580
- // #184 — "elsewhere: suggest, never install" half of the skills
581
- // bootstrap story (lib/skills.ts's bootstrapSkillsOnLogin is the
582
- // "install on login" half). Only on a clean exit — mirrors the
583
- // referral tip's own success-only precedent (lib/tips.ts, called only
584
- // after a run genuinely succeeds) and sidesteps a whole class of
585
- // parse-error edge cases where `currentCommand` never resolved (so
586
- // its own `--json` flag can't be read reliably here either). Running
587
- // AFTER the command completed also means a `login` that just
588
- // bootstrapped is already reflected in this nudge's own "any skill
589
- // installed?" read — no separate command-name exclusion needed, and
590
- // no double-message risk with the line `login` may have just printed.
591
243
  if (process.exitCode === undefined || process.exitCode === 0) {
592
244
  try {
593
245
  maybeSuggestSkillsInstall({
@@ -595,7 +247,6 @@ export async function runCli(argv = process.argv) {
595
247
  });
596
248
  }
597
249
  catch {
598
- // swallow — see skillsNudge.ts, this is already self-guarded too.
599
250
  }
600
251
  }
601
252
  }
package/dist/lib/api.d.ts CHANGED
@@ -1,102 +1,24 @@
1
1
  export declare class ApiError extends Error {
2
2
  status: number;
3
3
  next?: string[] | undefined;
4
- /**
5
- * `next` is an OPTIONAL per-instance override of the envelope's default
6
- * `next` steps (errors.ts's frozen `RETRY_POLICY`) — set only at the two
7
- * apiKey-mode 401 call sites below (via authNextSteps()), where the
8
- * generic "trawl login --token <jwt>" default is inert until a live
9
- * TRAWL_API_KEY/TRAWL_TOKEN is unset first (#169 review). Absent for every
10
- * other error, so classifyError falls back to the frozen default exactly
11
- * as before.
12
- */
13
4
  constructor(status: number, message: string, next?: string[] | undefined);
14
5
  }
15
- /**
16
- * A fetch-level failure — the request never got a response at all (DNS,
17
- * connection refused, timeout, TLS, …). Distinguished from ApiError (which
18
- * always carries a real HTTP status) so the top-level handler can map it to
19
- * its own exit code instead of the generic uniform 1. (#71 findings 4/58)
20
- */
21
6
  export declare class NetworkError extends Error {
22
7
  constructor(message: string);
23
8
  }
24
- /**
25
- * A LOCAL auth failure — no token available, or a locally-decoded token
26
- * that's provably expired, discovered entirely client-side before any HTTP
27
- * call was ever made. Distinguished from ApiError(401) (a real server-issued
28
- * 401 response) so the --json envelope never claims `status:401` for
29
- * something the server never said — that would be a fabricated fact,
30
- * indistinguishable from an actual server round-trip to a machine consumer.
31
- * Both classify to the same exit code (3) / kind "auth" in classifyError
32
- * (errors.ts); only the envelope's `status` field differs (present for
33
- * ApiError, absent here). (#88 item 4)
34
- */
35
9
  export declare class AuthError extends Error {
36
10
  next?: string[] | undefined;
37
- /** See ApiError's `next` for what this overrides and why. */
38
11
  constructor(message: string, next?: string[] | undefined);
39
12
  }
40
- /**
41
- * The single "no token available" error — every call site in this file that
42
- * needs a token (request/upload/getText/stream) used to throw its own copy
43
- * of `new Error('Not logged in. Run: trawl login')`, which fell through
44
- * classifyError's generic branch (exit 1, kind:"unknown") — indistinguishable
45
- * from an arbitrary bug. Auth-classifying it puts it on the exact same
46
- * exit-3 / kind:"auth" path a real 401 response already takes — but as an
47
- * AuthError (no HTTP call happened here), never a fabricated ApiError(401).
48
- * `trawl token` (src/commands/token.ts) reuses this too, so "no token" means
49
- * the same thing everywhere it can be observed. (#86 findings 1/2, #88 item 4)
50
- */
51
13
  export declare function notLoggedInError(): AuthError;
52
- /**
53
- * The "this route is JWT-only" error — thrown client-side, before any HTTP
54
- * call, by a command that only works with a session JWT (today: `whoami`,
55
- * see its own doc comment for why `GET /api/users/me` was descoped rather
56
- * than relocated) when the resolved credential is a scoped API key instead.
57
- * Distinct from notLoggedInError(): there IS a credential here, it's just
58
- * the wrong shape for this one route. Left unhandled, that route would 401
59
- * and surface the generic `notLoggedInError`-adjacent message ("Session
60
- * expired or invalid. Run: trawl login") — wrong twice over under a key: the
61
- * route never accepts keys at all (no session ever "expired"), and
62
- * re-running `trawl login` genuinely IS the fix here, just not because
63
- * anything expired. Classifies to the SAME exit 3 / kind:"auth" as every
64
- * other auth failure (#169) — reusing the existing `auth` kind rather than
65
- * minting a new one, since the taxonomy question is still "not
66
- * authenticated the way this route needs," never a new category.
67
- *
68
- * Always called already knowing authMode is 'apiKey' (see whoami.ts) — the
69
- * message and `next` both route through loginRemedyText()/authNextSteps()
70
- * so this never emits the same inert "Run: trawl login" that finding 2
71
- * corrects everywhere else. (#169 review round 2 — finding 2)
72
- */
73
14
  export declare function apiKeyUnsupportedError(command: string): AuthError;
74
- /**
75
- * #91 P0 — some endpoints legitimately run 30–250s server-side: a scrap
76
- * execute (worker-puppeteer navigation + antibot tier escalation + AI-fix
77
- * dry-run retries). The generic 30s default was aborting those mid-flight
78
- * and surfacing a fabricated `NetworkError timed out` (exit 5) for a request
79
- * that was always going to succeed given enough time. Passed as the per-call
80
- * `{timeoutMs}` override at exactly the 4 call sites that hit those
81
- * endpoints: `scraps run` / `data --fresh` (GET /api/scraps/load/:id) and
82
- * `scraps trigger --wait` (POST /api/scraps/worker/:id, synchronous branch
83
- * only — the default async `?wait=false` POST returns almost immediately and
84
- * keeps the 30s default) — all three in src/commands/scraps.ts — plus (#114)
85
- * `create` (POST /api/ai/wizard, src/commands/create.ts), whose AI-generation
86
- * + scrap creation + first run + autofix pipeline runs the same 30–250s+
87
- * server-side. 300s leaves margin over the ~250s worst case without being
88
- * unboundedly long.
89
- */
90
15
  export declare const LONG_RUN_TIMEOUT_MS = 300000;
91
16
  export interface RequestOptions {
92
- /** Per-call timeout override in ms (e.g. LONG_RUN_TIMEOUT_MS). Ignored — env wins — when TRAWL_TIMEOUT is set; see getTimeoutMs. */
93
17
  timeoutMs?: number;
94
18
  }
95
19
  export declare const api: {
96
20
  get: <T>(path: string, opts?: RequestOptions) => Promise<T>;
97
21
  publicGet: <T>(path: string, opts?: RequestOptions) => Promise<T>;
98
- /** See `probeJson`'s own doc comment — hard-bounded, probe-only, never for
99
- * a command's real work. Today's one caller: spec.ts's fetchExternalDocsUrl. */
100
22
  probeJson: <T>(path: string, opts: {
101
23
  timeoutMs: number;
102
24
  }) => Promise<T | null>;