@trawlme/cli 3.11.0 → 3.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +5 -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 +142 -656
  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 +31 -0
  24. package/dist/lib/cdp-pipe.js +141 -0
  25. package/dist/lib/chrome-discovery.d.ts +1 -0
  26. package/dist/lib/chrome-discovery.js +30 -0
  27. package/dist/lib/chrome-launch.d.ts +8 -0
  28. package/dist/lib/chrome-launch.js +53 -0
  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 +1 -0
  51. package/dist/lib/secure-transport.js +15 -0
  52. package/dist/lib/session-capture-guard.d.ts +6 -0
  53. package/dist/lib/session-capture-guard.js +9 -0
  54. package/dist/lib/session-capture.d.ts +55 -0
  55. package/dist/lib/session-capture.js +319 -0
  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 +55 -0
  63. package/dist/lib/storage-state.js +96 -0
  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/package.json +2 -2
@@ -11,8 +11,6 @@ function pickSkills(arg) {
11
11
  if (!arg || arg === 'all')
12
12
  return all;
13
13
  if (!all.includes(arg)) {
14
- // Usage error (exit 2), not a generic bug (exit 1) — the caller typed a
15
- // skill name that doesn't exist. (#86 finding 3)
16
14
  throw new UsageError(`Unknown skill "${arg}". Available: ${all.join(', ') || '(none)'}`);
17
15
  }
18
16
  return [arg];
@@ -39,19 +37,6 @@ skills
39
37
  for (const name of bundled) {
40
38
  const userInstalled = isSkillInstalled(name, 'user');
41
39
  const localInstalled = isSkillInstalled(name, 'local');
42
- // #91 — same EISDIR class as autoUpdateInstalledSkills/installSkill: an
43
- // unreadable `.version` marker on one skill must not crash the whole
44
- // listing before the other skills are shown.
45
- //
46
- // #93 item 3 — the two scope reads must be guarded INDEPENDENTLY. A
47
- // single try/catch wrapped around `getInstalledVersion(name, 'user') ??
48
- // getInstalledVersion(name, 'local')` still throws the whole expression
49
- // the moment the 'user' read throws (e.g. a corrupt/EISDIR `.version`
50
- // marker) — `??` never gets a chance to evaluate the 'local' fallback,
51
- // so a healthy local install gets masked as "not installed" too. Every
52
- // other marker read in this codebase (installSkill, removeOrphanedSkills,
53
- // autoUpdateInstalledSkills — all in lib/skills.ts) already guards each
54
- // scope on its own; this was the one chained exception.
55
40
  let installedVersion;
56
41
  try {
57
42
  installedVersion = getInstalledVersion(name, 'user');
@@ -155,13 +140,6 @@ skills
155
140
  }
156
141
  console.log(chalk.green(`✓ Updated "${name}"`) + chalk.dim(` at ${dest}`));
157
142
  }
158
- // #86 review — same orphan sweep as the startup auto-sync: an explicit
159
- // `skills update` must also drop CLI-owned dirs whose skill was renamed
160
- // or removed upstream (e.g. 1.0.0's `trawl` → 1.3.1's `trawl-cli`),
161
- // instead of leaving a stale ghost teaching outdated usage. Only the
162
- // scope being updated is swept; marker-less dirs are never touched.
163
- // removeOrphanedSkills prints its own honest stderr line per removal
164
- // (stderr, so it's safe under --json too).
165
143
  const orphansRemoved = removeOrphanedSkills(scope);
166
144
  if (opts.json)
167
145
  json({ updated: results, orphansRemoved });
@@ -1,14 +1,5 @@
1
1
  import { Command } from 'commander';
2
2
  import { type DocsUrls } from '../lib/docs.js';
3
- /**
4
- * `trawl spec --json` (#170) — a versioned, machine-readable description of
5
- * the command tree, so an agent can learn the CLI's surface without parsing
6
- * ~250 lines of README prose. Everything below is DERIVED at runtime from
7
- * the live commander `Command` tree (`buildSpec`) — never a hand-maintained
8
- * file, which would be a second source of truth that silently drifts. See
9
- * `tests/contracts/` for the README-vs-spec cross-check that exists
10
- * specifically to catch that drift.
11
- */
12
3
  export interface CliSpecArgument {
13
4
  name: string;
14
5
  required: boolean;
@@ -18,106 +9,30 @@ export interface CliSpecOption {
18
9
  long: string;
19
10
  short?: string;
20
11
  description: string;
21
- /**
22
- * commander's `Option#required`/`Option#mandatory` are a false-friend pair
23
- * that an earlier draft of this file collapsed into one `required` key —
24
- * DON'T "simplify" it back. `Option#required` means "if this option is
25
- * used, a value must follow" (true for any `<value>` flag, e.g. `--limit
26
- * <n>`, whether or not the option itself is optional to pass). `mandatory`
27
- * means "the user MUST pass this option at all" (only true for
28
- * `.requiredOption()`). Naming the value-arity field `required` (as this
29
- * did before) reads as "you must specify this option" — wrong for ~30 of
30
- * the ~34 options that tripped it. See commander's option.js:15/19.
31
- */
32
12
  mandatory: boolean;
33
13
  valueRequired: boolean;
34
14
  negatable: boolean;
35
15
  default?: unknown;
36
16
  }
37
17
  export interface CliSpecCommand {
38
- /** Full path from the program root, e.g. "scraps account session set". */
39
18
  name: string;
40
19
  description: string;
41
20
  hidden: boolean;
42
- /**
43
- * True when this node has no subcommands of its own — i.e. it is an
44
- * executable leaf (`.action()` actually runs something), not a pure
45
- * namespace/group node like `scraps`/`skills`/`telemetry`/`scraps account`.
46
- * An agent that emits one tool per spec entry needs this to skip group
47
- * nodes: invoking one directly (e.g. `trawl scraps`) exits non-zero with
48
- * usage text on stderr and ZERO bytes on stdout — a guaranteed-failing
49
- * tool, not a --json envelope. Derived the same way
50
- * `tests/contracts/readme-commands.test.ts` independently re-derives it
51
- * (its own `leafNames()` walk existed ONLY because this field didn't) —
52
- * that test now consumes THIS field instead of re-walking the tree itself.
53
- */
54
21
  leaf: boolean;
55
22
  aliases: string[];
56
23
  arguments: CliSpecArgument[];
57
24
  options: CliSpecOption[];
58
- /**
59
- * #185 — a deep link into a specific guide when one is mapped for this
60
- * command (docs.ts's `COMMAND_DOC_PATHS`, e.g. every `scraps account *`
61
- * command -> the account-sessions guide). Omitted (never a guessed/generic
62
- * link) whenever no guide is mapped, the top-level `docsUrl` itself
63
- * couldn't be resolved, OR `docsUrl` resolved from rung 1 (a server-
64
- * declared `externalDocs.url`) rather than rung 2 (this CLI's own known-host
65
- * derivation) — appending our guide slug onto a third party's own docs root
66
- * is exactly the confidently-wrong-404 defect this omission closes. See
67
- * `resolveCommandDocsUrl` / `DocsUrls.docsUrlIsDerived`.
68
- */
69
25
  docs?: string;
70
26
  }
71
27
  export interface CliSpec {
72
28
  specVersion: 1;
73
29
  cliVersion: string;
74
30
  commands: CliSpecCommand[];
75
- /** "0".."5" → a short kind label. Same constant `classifyError` reads
76
- * (errors.ts's EXIT_CODE_LABELS) — never a second copy. Code `1` is a
77
- * SHARED bucket (several `kind`s land there, see `kindExitCodes` below
78
- * for the honest reverse mapping) — this label names the generic/unmapped
79
- * case, not an exhaustive claim that `1` means only that. */
80
31
  exitCodes: Record<string, string>;
81
- /** Every `kind` a --json error envelope can carry — the classifyError
82
- * kinds PLUS the hand-built ones (`in_progress`/`run_failed`/
83
- * `upgrade_failed`). Same constant (errors.ts's ENVELOPE_KINDS) — never a
84
- * second copy, and never `ERROR_KINDS` (that one omits the hand-built
85
- * kinds by design — see its doc comment). */
86
32
  errorKinds: string[];
87
- /**
88
- * #170 review F7 — the inverse of `exitCodes`: `kind -> exitCode`, one
89
- * entry per `errorKinds` member. `exitCodes` alone can't tell an agent
90
- * which of the several kinds sharing code `1` (`api`/`refused`/`unknown`/
91
- * `in_progress`/`run_failed`/`upgrade_failed`) it actually got — this
92
- * field answers that directly instead of requiring the reverse lookup to
93
- * be reconstructed by hand. Same source constant as `errorKinds`
94
- * (errors.ts's KIND_EXIT_CODES) — never a second copy.
95
- */
96
33
  kindExitCodes: Record<string, number>;
97
- /**
98
- * #185 — where the human-readable guides live, resolved via docs.ts's
99
- * ladder (server `externalDocs.url` > derive from the API base's known
100
- * `trawl.me` host > omit). Absent (never a guessed URL) when neither rung
101
- * resolves — e.g. a self-hosted install with no `externalDocs` declared.
102
- */
103
34
  docsUrl?: string;
104
- /** #185 — the llms.txt entry point alongside `docsUrl`, when resolvable.
105
- * See docs.ts's module doc comment for why this never rides along with a
106
- * server-declared `docsUrl` it wasn't itself derived from. */
107
35
  llmsUrl?: string;
108
36
  }
109
- /**
110
- * Build the full spec from a live, already-constructed program (e.g.
111
- * `createProgram()`'s return value). Includes every node in the tree —
112
- * hidden legacy aliases (`scraps list`, …) included, flagged via `hidden`,
113
- * so a consumer that wants only the advertised surface can filter on it.
114
- *
115
- * #185 — `docs` is an already-RESOLVED `DocsUrls` (docs.ts's ladder run to
116
- * completion), never computed in here: `buildSpec` stays pure/synchronous
117
- * (no network, no config read) exactly as before, so every existing caller
118
- * (the README-vs-spec contract test, the synthetic-tree unit tests) keeps
119
- * working unchanged by simply omitting the parameter. Only `spec.ts`'s own
120
- * action resolves rung 1 (a bounded fetch) before calling this.
121
- */
122
37
  export declare function buildSpec(program: Command, docs?: DocsUrls): CliSpec;
123
38
  export declare const spec: Command;
@@ -37,18 +37,6 @@ function buildCommandEntry(cmd, name, hidden, docs = {}) {
37
37
  entry.docs = docsLink;
38
38
  return entry;
39
39
  }
40
- /**
41
- * Walk `cmd.commands` recursively, collecting one entry per node under its
42
- * FULL path name. `hidden` is read via commander's own
43
- * `Help#visibleCommands()` — the same idiom this codebase already uses
44
- * (scraps.test.ts's #108 describe block) — rather than reaching for the
45
- * private, untyped `_hidden` field directly. The resolved `docs` object,
46
- * when present, threads through so every entry can carry its own per-command
47
- * `docs` deep link (#185) off the SAME resolved root — never a second lookup
48
- * per node, and never flattened to a bare string (see docs.ts's
49
- * `DocsUrls.docsUrlIsDerived` — the rung provenance must survive the walk
50
- * unchanged so `resolveCommandDocsUrl` can still gate on it at each leaf).
51
- */
52
40
  function walk(cmd, prefix, out, docs = {}) {
53
41
  const visible = new Set(new Help().visibleCommands(cmd));
54
42
  for (const sub of cmd.commands) {
@@ -57,19 +45,6 @@ function walk(cmd, prefix, out, docs = {}) {
57
45
  walk(sub, name, out, docs);
58
46
  }
59
47
  }
60
- /**
61
- * Build the full spec from a live, already-constructed program (e.g.
62
- * `createProgram()`'s return value). Includes every node in the tree —
63
- * hidden legacy aliases (`scraps list`, …) included, flagged via `hidden`,
64
- * so a consumer that wants only the advertised surface can filter on it.
65
- *
66
- * #185 — `docs` is an already-RESOLVED `DocsUrls` (docs.ts's ladder run to
67
- * completion), never computed in here: `buildSpec` stays pure/synchronous
68
- * (no network, no config read) exactly as before, so every existing caller
69
- * (the README-vs-spec contract test, the synthetic-tree unit tests) keeps
70
- * working unchanged by simply omitting the parameter. Only `spec.ts`'s own
71
- * action resolves rung 1 (a bounded fetch) before calling this.
72
- */
73
48
  export function buildSpec(program, docs = {}) {
74
49
  const commands = [];
75
50
  walk(program, '', commands, docs);
@@ -87,40 +62,7 @@ export function buildSpec(program, docs = {}) {
87
62
  spec.llmsUrl = docs.llmsUrl;
88
63
  return spec;
89
64
  }
90
- /**
91
- * #185 — bounds the SOCKET, not just the promise, so a slow/dead server can
92
- * never perceptibly slow `spec --json` down. Mirrors lib/tips.ts's
93
- * FLAG_CHECK_TIMEOUT_MS for the same reason: this is the one surface allowed
94
- * a network call at all, and only because it's a deliberate, once-per-
95
- * invocation agent probe.
96
- *
97
- * This is a REAL wall-clock ceiling, not merely a promise-level one: an
98
- * earlier version of this fetch used `api.publicGet` (Node's global
99
- * `fetch()` + `AbortSignal.timeout()`), which bounds the promise but not the
100
- * OS-level connection — against a routable-but-silently-dropping host (a
101
- * dropped SYN, no RST; a realistic firewalled/air-gapped shape, the very
102
- * audience this feature cites) the promise rejected at ~2005ms while the
103
- * PROCESS didn't exit until ~10.5s later, waiting out Node's own internal
104
- * connect-timeout. `api.probeJson` (see its own doc comment in lib/api.ts)
105
- * fixes this by managing the raw socket directly — an independent timer
106
- * that `req.destroy()`s the connection, never relying on the idle-based
107
- * `http.request` `timeout` option. Re-measured after the fix, same
108
- * black-holed host, via the exact repro command (`time env
109
- * TRAWL_API_URL=https://192.0.2.1 node dist/index.js spec --json`): total
110
- * process time ~2.43s/~2.47s across two runs (the 2000ms bound plus Node
111
- * startup and spec-tree construction), not ~10.68s.
112
- */
113
65
  const EXTERNAL_DOCS_FETCH_TIMEOUT_MS = 2_000;
114
- /**
115
- * Rung 1 — read `externalDocs.url` off the OpenAPI document the configured
116
- * API base itself serves at `/api/spec.json`. Unset (null) server-side
117
- * today, so this resolves to `null` in practice; forward-looking for when
118
- * it isn't. ANY failure — network error, timeout, a malformed/unexpected
119
- * response shape — is indistinguishable from "not declared" here: resolves
120
- * to `null`, never throws (api.probeJson's own contract — see its doc
121
- * comment). `spec --json` must stay a pure, reliable, side-effect-free probe
122
- * even against an unreachable or ancient server.
123
- */
124
66
  async function fetchExternalDocsUrl() {
125
67
  const data = await api.probeJson('/api/spec.json', {
126
68
  timeoutMs: EXTERNAL_DOCS_FETCH_TIMEOUT_MS,
@@ -132,17 +74,8 @@ export const spec = new Command('spec')
132
74
  .description('Print a versioned, machine-readable description of the command tree')
133
75
  .option('--json', 'Output as JSON')
134
76
  .action(async (opts) => {
135
- // `spec` is registered as a direct child of the program root
136
- // (index.ts's createProgram), so `.parent` IS that root by the time this
137
- // action ever runs — commander sets it in `addCommand()`. Falling back
138
- // to `spec` itself only matters for an isolated unit invocation (e.g. a
139
- // test driving `spec.parseAsync()` directly, unattached to a program).
140
77
  const root = spec.parent ?? spec;
141
78
  const apiBaseUrl = getApiUrl();
142
- // #185 — rung 1 (the server fetch) only runs under --json: the
143
- // plain-text branch below never reads docsUrl at all, so paying a
144
- // network round-trip for it would be pure waste. Rungs 2/3 (sync,
145
- // no network) still apply either way via the fallback below.
146
79
  const externalDocsUrl = opts.json ? await fetchExternalDocsUrl() : null;
147
80
  const docs = resolveDocsUrls({ apiBaseUrl, externalDocsUrl });
148
81
  const cliSpec = buildSpec(root, docs);
@@ -10,7 +10,6 @@ telemetry
10
10
  .option('--json', 'Output as JSON')
11
11
  .action((opts) => {
12
12
  config.set('telemetry', true);
13
- // Ensure a telemetryUserId is created on opt-in
14
13
  initPostHog();
15
14
  if (opts.json) {
16
15
  json({ telemetry: true });
@@ -41,9 +40,6 @@ telemetry
41
40
  const userId = config.get('telemetryUserId') || '(not yet generated)';
42
41
  const envOverride = process.env['TRAWL_TELEMETRY'] === '0';
43
42
  const doNotTrack = process.env['DO_NOT_TRACK'] === '1';
44
- // The effective on/off state factoring in both env overrides — matches
45
- // the human-mode "State:" line's own precedence (DO_NOT_TRACK >
46
- // TRAWL_TELEMETRY > config).
47
43
  const effectiveEnabled = enabled && !envOverride && !doNotTrack;
48
44
  if (opts.json) {
49
45
  json({ enabled: effectiveEnabled, configEnabled: enabled, telemetryUserId: config.get('telemetryUserId') || null, envOverride, doNotTrack });
@@ -9,35 +9,16 @@ export const token = new Command('token')
9
9
  .description('Print the stored credential — a scoped API key or a session JWT (for MCP Bearer auth)')
10
10
  .option('--json', 'Output as JSON ({token, exp, expiresAt, mode}) instead of the raw credential')
11
11
  .action((opts) => {
12
- // getToken() resolves TRAWL_API_KEY env first, then TRAWL_TOKEN env, then
13
- // the stored config token (see config.ts) — matching every other token
14
- // consumer in the CLI instead of reading the config store directly. (#86
15
- // finding 1, #169)
16
12
  const stored = getToken();
17
13
  if (!stored) {
18
- // Auth-classified (ApiError 401 → exit 3, kind:"auth"), not a generic
19
- // exit 1 — an agent scripting `trawl token` needs to tell "not logged
20
- // in" apart from an arbitrary bug. (#86 finding 1)
21
14
  process.exitCode = reportError(notLoggedInError(), { json: opts.json });
22
15
  return;
23
16
  }
24
- // #169 review finding 2 — a scoped API key is not a JWT: it has no `exp`
25
- // claim to decode (decodeExp() would return null for one anyway, since
26
- // it isn't dot-segmented), and unlike a session JWT it does NOT expire
27
- // on its own — it stays live until revoked in the dashboard. Branching
28
- // on getAuthMode() explicitly (rather than silently relying on
29
- // decodeExp()'s null-for-non-JWT behaviour) means the distinct advisory
30
- // below is a deliberate case, not an accident of what decodeExp() happens
31
- // to return — and it replaces the old "(could not decode expiry — verify
32
- // the token manually)" fallback, which was written for a malformed JWT
33
- // and, read against a key, wrongly implied something was wrong with it.
34
17
  if (getAuthMode(stored) === 'apiKey') {
35
18
  if (opts.json) {
36
19
  json({ token: stored, exp: null, expiresAt: null, mode: 'apiKey' });
37
20
  return;
38
21
  }
39
- // Print the raw key first (so it can be piped / copied) — same
40
- // stdout-only-the-credential contract `$(trawl token)` relies on.
41
22
  console.log(stored);
42
23
  console.error(chalk.dim(' This is a scoped API key (trawl_*) — it does not expire on its own. Revoke it in the dashboard to end its access.'));
43
24
  return;
@@ -45,9 +26,6 @@ export const token = new Command('token')
45
26
  const exp = decodeExp(stored);
46
27
  const nowSeconds = Math.floor(Date.now() / 1000);
47
28
  if (exp !== null && exp < nowSeconds) {
48
- // Decoded entirely client-side (no HTTP call made) — AuthError, not a
49
- // fabricated ApiError(401): the server never actually said this. (#88
50
- // item 4)
51
29
  process.exitCode = reportError(new AuthError('Session token expired. Run: trawl login to refresh.'), { json: opts.json });
52
30
  return;
53
31
  }
@@ -60,10 +38,8 @@ export const token = new Command('token')
60
38
  });
61
39
  return;
62
40
  }
63
- // Print the raw token first (so it can be piped / copied)
64
41
  console.log(stored);
65
42
  if (exp === null) {
66
- // Could not decode expiry (malformed/opaque JWT) — advisory only, do not block piping
67
43
  console.error(chalk.dim(' (could not decode expiry — verify the token manually)'));
68
44
  }
69
45
  else {
@@ -71,10 +47,6 @@ export const token = new Command('token')
71
47
  const daysLeft = secsLeft / 86400;
72
48
  if (daysLeft < 1) {
73
49
  const hoursLeft = Math.floor(secsLeft / 3600);
74
- // stderr, not stdout — this is an advisory, not the payload. `trawl
75
- // token` exists so callers can capture the raw JWT via
76
- // `$(trawl token)` for `Authorization: Bearer …`; anything printed
77
- // to stdout after the token corrupts that capture. (#68)
78
50
  console.error(chalk.yellow(`⚠ Token expiring in ${hoursLeft}h. Run: trawl login to refresh.`));
79
51
  }
80
52
  else {
@@ -21,21 +21,12 @@ export const upgrade = new Command('upgrade')
21
21
  return;
22
22
  }
23
23
  if (opts.check) {
24
- // #170 review F10 — set BEFORE the --json early return. Before this,
25
- // `trawl upgrade --check` exited 1 but `trawl upgrade --check --json`
26
- // (identical state) exited 0 — the exit code was set below the --json
27
- // `return`, so a --json caller never saw it. README documents this gate
28
- // as "exit 1 if so" with no --json carve-out; a CI step gating on the
29
- // exit code alone silently never fired under --json.
30
24
  process.exitCode = 1;
31
25
  if (opts.json)
32
26
  return json({ package: PKG_NAME, current, latest, upToDate: false, upgraded: false });
33
27
  console.log(`${chalk.yellow('↑')} Update available: ${chalk.bold(current)} → ${chalk.bold(latest)}. Run ${chalk.cyan('trawl upgrade')} to install.`);
34
28
  return;
35
29
  }
36
- // Install the latest globally. This shells out to the same npm the user
37
- // installed the CLI with; a permission error (EACCES on a system-owned
38
- // global prefix) is surfaced with the manual command rather than swallowed.
39
30
  if (!opts.json)
40
31
  console.log(chalk.dim(`Upgrading ${PKG_NAME}: ${current} → ${latest}…`));
41
32
  try {
@@ -49,19 +40,6 @@ export const upgrade = new Command('upgrade')
49
40
  ? 'permission denied on the global npm prefix — retry with sudo, or use a Node version manager.'
50
41
  : (e.stderr?.trim() || e.message);
51
42
  if (opts.json) {
52
- // `upgrade_failed` is intentionally NOT in errors.ts's RETRY_POLICY
53
- // (that map is the exhaustive set of kinds classifyError can
54
- // produce — ERROR_KINDS derives from its keys) — retryFieldsFor is
55
- // total, so an unmapped kind still gets the honest conservative
56
- // default (`retryable:false`) instead of silently omitting the
57
- // field. It IS registered in ENVELOPE_KINDS, the superset
58
- // `spec --json`'s errorKinds actually publishes — see that
59
- // constant's doc comment.
60
- //
61
- // #170 review F3 — typed as ErrorEnvelope so the compiler enforces
62
- // `retryable` here too (this was one of the two emitters a
63
- // mutation-test proved `tsc --noEmit` never actually guarded before
64
- // this annotation).
65
43
  const envelope = {
66
44
  message: `upgrade failed: ${hint}`,
67
45
  kind: 'upgrade_failed',
@@ -1,16 +1,4 @@
1
1
  import { Command } from 'commander';
2
- /**
3
- * `GET /api/users/me` response shape (users.account.controller.js#me).
4
- *
5
- * Chosen over the MCP `trawl_whoami` tool's own payload
6
- * (userId/organizationId/organizationName/scrapIdsAllowlist/mode) because
7
- * that shape lives entirely in the MCP per-call auth-bridge context
8
- * (developers.mcp.js) — there is no equivalent REST route exposing
9
- * organization/scope/mode for a session-JWT caller today. `/api/users/me`
10
- * is the closest REST parity match: real identity, already used by every
11
- * other trawl_node client. `roles` is the nearest analogue to "scopes";
12
- * there is currently no "plan" field on this endpoint.
13
- */
14
2
  export interface WhoamiResponse {
15
3
  id?: string;
16
4
  email?: string;
@@ -7,12 +7,6 @@ export const whoami = new Command('whoami')
7
7
  .description("Show the authenticated user's identity")
8
8
  .option('--json', 'Output as JSON')
9
9
  .action(async (opts) => {
10
- // #169 — JWT-only route (see WhoamiResponse's own doc comment above for
11
- // why it stays that way). Under a scoped API key, a real request here
12
- // would 401 and surface the generic "Session expired or invalid" text —
13
- // wrong twice over: this route never accepts keys at all, and no
14
- // session ever "expired". Detect it client-side, before any HTTP call,
15
- // and say so plainly instead.
16
10
  if (getAuthMode() === 'apiKey') {
17
11
  throw apiKeyUnsupportedError('trawl whoami');
18
12
  }
package/dist/index.d.ts CHANGED
@@ -1,203 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- /**
4
- * Derive a safe telemetry event name from a *resolved* commander Command —
5
- * NEVER from raw argv. Flag values (e.g. the string after --password/--email/
6
- * --url) don't start with '-' and would otherwise survive an argv filter and
7
- * leak to PostHog (#67). Falls back to 'unknown' when no command resolved
8
- * (e.g. an error thrown before any action ran).
9
- *
10
- * #88 item 9 — walks the FULL parent chain, not just the immediate parent:
11
- * the old one-level join (`${parent.name()} ${own.name()}`) resolved a
12
- * 4-deep command like `scraps account session set` down to just "session
13
- * set", silently dropping "scraps account". The root program node (the
14
- * 'trawl' Command itself, which has no `.parent`) is excluded from the
15
- * chain — matching the pre-existing convention that a direct child of the
16
- * root (e.g. `scraps list`, `telemetry on`) is named relative to its
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.
26
- */
27
3
  export declare function resolveCommandName(actionCommand: Command | undefined): string;
28
- /**
29
- * Walk the full command tree and produce every valid resolveCommandName()
30
- * token — the allowlist registered with posthog.ts so captureCommand can
31
- * never be handed a free-form string.
32
- */
33
4
  export declare function collectCommandNames(root: Command): string[];
34
5
  export declare function createProgram(): Command;
35
- /**
36
- * #185 — see the `addHelpText('afterAll', …)` call above for why this is
37
- * unconditional (no TTY/--json gate) and why a thrown error must never
38
- * escape: commander calls this synchronously while already writing help
39
- * output, so a throw here would crash a `--help` invocation, the one code
40
- * path this whole feature is not allowed to touch (see docs.ts's own
41
- * `docsFooterLine` for the "never a guessed URL" contract this composes).
42
- */
43
6
  export declare function docsFooterText(): string;
44
- /**
45
- * True when this module is the process entrypoint (not merely imported by a test).
46
- *
47
- * npm installs the global bin as a SYMLINK (`bin/trawl` -> `dist/index.js`). For an
48
- * ESM entrypoint Node realpaths the module URL (`import.meta.url` = the real
49
- * `dist/index.js`) but leaves `process.argv[1]` as the symlink path — so a raw
50
- * `moduleUrl === pathToFileURL(argv1).href` compare is FALSE for the normal global
51
- * invocation, the guard never fires, and `runCli()` never runs (silent no-op, #103).
52
- * Canonicalise both sides with `realpathSync` before comparing. A genuine entrypoint
53
- * was just loaded by Node, so both realpath calls resolve; the catch only trips for a
54
- * non-file module URL (exotic loaders) — correctly "not the entrypoint".
55
- */
56
7
  export declare function isEntryPoint(argv1: string | undefined, moduleUrl: string): boolean;
57
- /**
58
- * True when the invocation is a pure `--help`/`--version` query, a bare
59
- * `trawl` with no subcommand (commander prints top-level help and exits), or
60
- * `trawl help [command]`. None of these should trigger the skills auto-sync
61
- * (a filesystem-mutating startup side effect) — a user running `trawl
62
- * --version` (or just `trawl`) never expects it to rewrite their skills
63
- * dirs. (#73, extended #86 finding 7 for the bare-invocation + `help`
64
- * subcommand cases)
65
- */
66
8
  export declare function isHelpOrVersion(argv: string[]): boolean;
67
- /**
68
- * True for a bare `trawl` invocation — no subcommand, no flags at all. This
69
- * is the CLI's "first thing a new user sees" moment (commander prints its
70
- * own top-level help right after), distinct from `isHelpOrVersion` which is
71
- * intentionally broader (also matches `--help`/`--version`/`trawl help`
72
- * anywhere in argv) — the Pinch wave banner (#94) only wants the narrowest
73
- * case so it never shows up ahead of e.g. `trawl scraps --help`.
74
- */
75
9
  export declare function isBareInvocation(argv: string[]): boolean;
76
- /**
77
- * True for `trawl spec` (with or without `--json`) — deliberately NOT folded
78
- * into `isHelpOrVersion` above: that predicate's name and doc comment are
79
- * about help/version queries specifically, and `spec` is a real, data-
80
- * bearing command (see `isHelpOrVersion`'s own #170 test case), not a help
81
- * query. This is its own narrow predicate for a DIFFERENT reason: `spec
82
- * --json` is documented (docs/agent-quickstart.md) as the first call an AI
83
- * agent makes against this CLI, so it must not mutate anything the caller
84
- * did not ask it to — same requirement `--help`/`--version` already got
85
- * from `isHelpOrVersion` (#73), extended here to cover `spec` too.
86
- *
87
- * Scope, stated honestly rather than aspirationally: this guard suppresses
88
- * the skills auto-sync (an `rmSync(recursive)` + `cpSync` over the user's
89
- * `~/.claude/skills`) and the update notifier (a config write plus a
90
- * DETACHED CHILD that queries the npm registry — fatal in an
91
- * egress-restricted sandbox). It does NOT suppress `initPostHog()`, which
92
- * still creates the `conf` config file and mints a persistent
93
- * `telemetryUserId` on first run, exactly as it does for every other
94
- * command. That one is deliberate: `spec` is the signal that tells us
95
- * whether agents are actually adopting the CLI, so it stays measured, and
96
- * the caller's own controls (`TRAWL_TELEMETRY=0`, `DO_NOT_TRACK=1`) are the
97
- * opt-out. That same first run also has `initPostHog()` write a one-time
98
- * telemetry disclosure line (`ℹ Trawl CLI collects anonymous usage
99
- * telemetry…`) to STDERR, ahead of the JSON payload — this guard does not
100
- * suppress that either. `stdout` stays pure JSON either way, so this only
101
- * bites a caller that merges the two streams (e.g. `2>&1`); whether a `spec`
102
- * probe specifically should suppress the notice is a product call,
103
- * deliberately not taken here. Do not upgrade this paragraph back to "no
104
- * filesystem side effect" without also gating telemetry — a comment that
105
- * overstates its own invariant is worse than no comment, because the next
106
- * reader trusts it.
107
- *
108
- * #185 — `spec --json` (not plain `spec`) now DOES make its own bounded
109
- * (2s), swallowed-on-failure GET for `externalDocs.url` (see spec.ts's
110
- * `fetchExternalDocsUrl`) — this is not a regression of the "no mutation"
111
- * invariant above (a GET mutates nothing, and any failure — including no
112
- * egress at all — degrades to the sync host-derivation fallback, never a
113
- * thrown error), just a second, narrower kind of side effect this guard was
114
- * never meant to suppress in the first place.
115
- *
116
- * Before this, `trawl spec --json` silently re-synced
117
- * `~/.claude/skills` (and `./.claude/skills`) and printed `trawl: re-synced
118
- * skill …` lines to stderr BEFORE the JSON payload — an agent's very first
119
- * probe of the CLI mutated the user's filesystem and polluted the channel
120
- * it was about to parse.
121
- *
122
- * Matches `spec` as the FIRST positional token, skipping any leading
123
- * `-`-prefixed tokens first (a global flag ahead of the subcommand, e.g.
124
- * `trawl --debug spec --json`) — the same argv-scan discipline `hasJsonFlag`
125
- * below already applies. #170 review F4 — before this, a bare
126
- * `argv.slice(2)[0] === 'spec'` check was defeated by ANY leading flag:
127
- * `trawl --debug spec --json` read `--debug` as the first positional, missed
128
- * the match entirely, and fell through to the normal startup path — running
129
- * the filesystem-mutating skills auto-sync during what's documented
130
- * (docs/agent-quickstart.md) as an agent's read-only first probe of the CLI.
131
- * Still never a bare substring search: e.g. `trawl scraps create --title
132
- * spec` (the literal string "spec" as a FLAG VALUE, not a leading flag) never
133
- * matches, since `scraps` — the first non-`-`-prefixed token — isn't `spec`.
134
- */
135
10
  export declare function isSpecQuery(argv: string[]): boolean;
136
- /** Best-effort scan for a `--json` flag in raw argv, used only when parsing
137
- * itself failed before any command's own `.opts()` could be resolved (a
138
- * commander usage error — unknown option/command, missing required arg). Same
139
- * "argv scan, never trust flag values" caveat as isHelpOrVersion: positional
140
- * values are never mistaken for `--json` since they don't equal the literal
141
- * string. (#86 finding 3)
142
- *
143
- * #88 item 5 — only scans tokens BEFORE the first bare `--`. Commander treats
144
- * `--` as "end of options": everything after it is a positional operand, not
145
- * a flag, even if the literal text is `--json`. `scraps list -- --json`
146
- * passes `--json` as an (excess) positional argument, not the flag — an
147
- * unscoped `argv.includes('--json')` would still match it and wrongly emit a
148
- * JSON envelope for what is actually a plain usage error with no --json
149
- * requested at all.
150
- */
151
11
  export declare function hasJsonFlag(argv: string[]): boolean;
152
- /**
153
- * Commander's default (no exitOverride) calls `process.exit()` directly for
154
- * a usage error (unknown option/command, missing required arg) or a
155
- * --help/--version/`help` query — bypassing runCli's try/catch/finally
156
- * entirely, so the telemetry shutdown() flush below never runs and a usage
157
- * error exits 1 (the generic bug bucket) instead of its own distinct code.
158
- * `program.exitOverride()` on the root command alone does NOT fix this for
159
- * subcommands added via `addCommand()` (login/scraps/skills/telemetry/token
160
- * are each built as standalone Command instances in their own module and
161
- * only ever copy inherited settings — including exitOverride — from a parent
162
- * at `.command()` construction time, which for these root-level modules never
163
- * happens). Every node in the tree needs its own exitOverride() call, so this
164
- * walks the whole tree and installs it everywhere. (#86 finding 3)
165
- */
166
12
  export declare function applyExitOverride(cmd: Command): void;
167
- /**
168
- * #149 item 1 — commander's own usage-error line (unknown option, missing
169
- * required arg, unknown command, …) is written via `Command#error()` ->
170
- * `outputError()` straight to `process.stderr` BEFORE the exitOverride throw
171
- * below is ever caught — in commander's own plain, uncolored `"error: …"`
172
- * format, a visibly different convention from every other error this CLI
173
- * prints (`reportError`'s `chalk.red('✗ ' + message)`). `configureOutput`'s
174
- * `outputError` hook is exactly the one commander documents for reformatting
175
- * the error text itself (`writeErr` also carries unrelated help output, e.g.
176
- * `showHelpAfterError` — never enabled in this CLI, but out of scope to
177
- * touch here). Overriding it on every node in the tree (same walk shape as
178
- * applyExitOverride, for the same reason: addCommand()-attached subtrees
179
- * don't otherwise inherit a parent's configureOutput) reformats that one line
180
- * through the same red ✗ convention, stripping the redundant "error: " prefix
181
- * first (stripCommanderErrorPrefix — the same prefix also leaks verbatim
182
- * into the --json envelope's `message` field, see runCli's catch below).
183
- */
184
13
  export declare function applyOutputConfiguration(cmd: Command): void;
185
- /**
186
- * #88 item 6 — best-effort command-name recovery for a commander parse error
187
- * that happened BEFORE any action ran, so `currentCommand` (the preAction
188
- * hook's resolved command) is still undefined — e.g. an unknown option on an
189
- * otherwise-valid subcommand, or excess arguments. Without this, EVERY such
190
- * failure previously collapsed to resolveCommandName(undefined) === 'unknown'
191
- * — and since 'unknown' was never itself allowlisted at the
192
- * registerAllowedCommands call site (see runCli below), that capture was
193
- * silently dropped by captureCommand's allowlist check: a dead branch that
194
- * looked like it reported telemetry but never actually did.
195
- *
196
- * Matches ONLY a name already present in `allowedNames` (the real command
197
- * tree, from collectCommandNames) — never invents one from raw argv text.
198
- * Checks the two-token form first (`scraps boom`) since most usage errors
199
- * happen on a nested leaf command; falls back to the single top-level token,
200
- * then to 'unknown' (now itself allowlisted, so that capture fires too).
201
- */
202
14
  export declare function bestEffortCommandName(argv: string[], allowedNames: readonly string[]): string;
203
15
  export declare function runCli(argv?: string[]): Promise<void>;