@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
@@ -28,60 +28,11 @@ async function promptEmail() {
28
28
  rl.close();
29
29
  }
30
30
  }
31
- /** #107 — the shared success-reporting tail for all three login paths (env
32
- * token, --token flag, email/password). Under --json, prints a single
33
- * structured result on stdout instead of the chalk lines — never the raw
34
- * token itself (that's what `trawl token` is for). */
35
31
  function reportLoginSuccess(opts, email) {
36
- // #169 review finding 3 — stderr, both under --json and plain output: this
37
- // is a warning about what happens AFTER login succeeds, not part of either
38
- // payload shape, so it must never be silently dropped just because --json
39
- // is set.
40
32
  const overrideVar = getLiveAuthEnvVar();
41
33
  if (overrideVar) {
42
34
  console.error(chalk.yellow(`⚠ ${overrideVar} is set in your environment — it overrides the token just stored here for every subsequent command until you unset it.`));
43
35
  }
44
- // #184 — `login` is the ONE intentional human setup moment: bootstrap any
45
- // bundled Claude skill the user doesn't have yet. Gated exactly like
46
- // every other filesystem-mutating side effect in this CLI
47
- // (json/TTY/opt-out — see lib/skills.ts's isSkillsActionAllowed), so a
48
- // CI/agent `TRAWL_TOKEN=x trawl login --json` never writes into
49
- // ~/.claude/skills on a machine with no Claude Code session to discover
50
- // them. `isInteractive` is the exact TTY definition login already uses
51
- // for its own email/password prompt gate (lib/confirm.ts) — stdin AND
52
- // stdout, not just stdout. `bootstrapped` is `null` ONLY when gated out
53
- // (json/non-TTY/opted-out) or when every bundled skill is already owned
54
- // by trawl — the genuine "nothing to do" case, where the lines below
55
- // naturally never print. It is non-null whenever anything was attempted,
56
- // whether or not any of it actually landed — see the block below.
57
- //
58
- // stderr, same convention as the `overrideVar` warning right above (and
59
- // for the same reason): this is orthogonal to the `--json` envelope's
60
- // fixed shape below, so it must never risk landing on stdout — a defense
61
- // that holds even if `bootstrapSkillsOnLogin`'s own json/TTY gate above it
62
- // were ever wrong, not just a style match.
63
- //
64
- // #184 review (BLOCK + MAJOR) — `bootstrapped` is non-null whenever there
65
- // was anything to attempt, whether or not any of it actually landed, so
66
- // both halves below must be checked independently: `installed` prints the
67
- // success line, `skipped` prints one honest line per skill that was
68
- // requested but did NOT land (a permission error, or a pre-existing
69
- // marker-less dir the ownership guard refused to overwrite) and WHY —
70
- // `reason` is already a relayable, fact-only string (SkillOwnershipRefusalError's
71
- // `.relayableReason`, or a raw fs error message) with no imperative, so it
72
- // is safe to print verbatim on this channel. A total failure (installed
73
- // empty, skipped non-empty) must read as "attempted and failed" — never
74
- // fall through to silence, which is indistinguishable from "never
75
- // attempted" (the exact false-success shape this issue exists to
76
- // prevent). The restart note only applies to what actually landed, so it
77
- // stays scoped to that branch.
78
- //
79
- // #184 defect 2 — `error` is the THIRD case: distinct from both "nothing
80
- // to do" (bootstrapped is `null`, nothing prints) and "some/all skills
81
- // failed" (`skipped`, above) — it means bootstrapSkillsOnLogin could not
82
- // even determine which skills to install (the bundled skills package
83
- // looks missing/corrupted). Stated as a fact, same convention as every
84
- // other line here.
85
36
  const bootstrapped = bootstrapSkillsOnLogin({ json: opts.json, isTTY: isInteractive(opts) });
86
37
  if (bootstrapped) {
87
38
  if (bootstrapped.error) {
@@ -113,17 +64,11 @@ export const login = new Command('login')
113
64
  .option('-p, --password <password>', 'Password (CI only — visible in process list and shell history)')
114
65
  .option('--json', 'Output as JSON')
115
66
  .action(async (opts) => {
116
- // Validate --url but do NOT persist it yet. A failed signin must not
117
- // brick the config by pointing it at an unreachable/wrong host while the
118
- // OLD token stays stored (and would then be sent to that new host on the
119
- // next command). Target this run via `pendingUrl` and persist only once
120
- // a token has actually been obtained. (#68)
121
67
  const pendingUrl = opts.url ? requireUrl(opts.url, '--url') : undefined;
122
68
  const persistUrlIfPending = () => {
123
69
  if (pendingUrl)
124
70
  config.set('apiUrl', pendingUrl);
125
71
  };
126
- // Check environment variable override first
127
72
  const envToken = process.env['TRAWL_TOKEN'];
128
73
  if (envToken) {
129
74
  config.set('token', requireFreshJwt(envToken, 'TRAWL_TOKEN'));
@@ -131,20 +76,15 @@ export const login = new Command('login')
131
76
  reportLoginSuccess(opts);
132
77
  return;
133
78
  }
134
- // --token flag: direct JWT (CI / retrocompat)
135
79
  if (opts.token) {
136
80
  config.set('token', requireFreshJwt(opts.token, '--token'));
137
81
  persistUrlIfPending();
138
82
  reportLoginSuccess(opts);
139
83
  return;
140
84
  }
141
- // Email/password flow
142
85
  if (opts.password) {
143
86
  console.error(chalk.yellow('Warning: passing --password on the command line is insecure.'));
144
87
  }
145
- // #107 — never blocks on a readline prompt under --json or a non-TTY
146
- // invocation (agent/CI subprocess); refuses with a clear, structured
147
- // usage error instead, naming the flag (or TRAWL_TOKEN) to pass.
148
88
  if (!opts.email) {
149
89
  requireInteractive('Email is required — pass -e/--email, --token, or set TRAWL_TOKEN (refusing to block on a prompt, non-interactive).', { json: opts.json });
150
90
  }
@@ -154,7 +94,6 @@ export const login = new Command('login')
154
94
  const email = opts.email ?? (await promptEmail());
155
95
  const password = opts.password ?? (await promptPassword('Password: '));
156
96
  const { data, headers } = await api.publicPost('/api/auth/signin', { email, password }, pendingUrl);
157
- // Try token from response body first, then fall back to Set-Cookie header
158
97
  let raw = typeof data === 'string' ? data : data.token;
159
98
  if (!raw) {
160
99
  const setCookie = headers.get('set-cookie') ?? '';
@@ -175,12 +114,6 @@ export const logout = new Command('logout')
175
114
  .option('--json', 'Output as JSON')
176
115
  .action((opts) => {
177
116
  config.set('token', '');
178
- // #169 review finding 3 — this is the sharpest version of the gap: an
179
- // operator runs `logout` SPECIFICALLY to kill access, and if
180
- // TRAWL_API_KEY/TRAWL_TOKEN is set, access is NOT killed — every
181
- // subsequent command keeps authenticating with the env credential this
182
- // command cannot touch. The wording below must not read as a variant of
183
- // "logged out"; it says plainly that access is still live.
184
117
  const overrideVar = getLiveAuthEnvVar();
185
118
  if (overrideVar) {
186
119
  console.error(chalk.yellow(`⚠ ${overrideVar} is still set in your environment — access is NOT revoked. Every subsequent command will keep authenticating with it until you unset it (or revoke the key in the dashboard).`));
@@ -1,19 +1,4 @@
1
1
  import { Command } from 'commander';
2
- /**
3
- * `GET /api/health` response shape (home.controller.js#health,
4
- * home.service.js#getHealthStatus) — MCP `trawl_health_ping` parity
5
- * (`{ok, version, uptime}`). The REST route enriches the payload with
6
- * `version`/`uptime`/`db`/`memory` ONLY for an admin caller
7
- * (home.controller.js's `isAdmin` gate); a non-admin JWT gets `{status:'ok'}`
8
- * alone on a healthy server. A non-2xx response (degraded, `status:503`)
9
- * throws an ApiError like any other failed request — but a 2xx response is
10
- * NOT a guarantee that `status === 'ok'` (#106 review F5): a soft-degraded
11
- * signal (e.g. a partial dependency outage) could still come back as a 200
12
- * with a non-"ok" `status`. The HTTP call not throwing only means the
13
- * transport succeeded, never that the reported health is good — so the
14
- * human-mode `✓ OK` line is gated on the actual field, not assumed from a
15
- * successful round-trip.
16
- */
17
2
  export interface PingResponse {
18
3
  status: string;
19
4
  db?: string;
@@ -6,33 +6,18 @@ export const ping = new Command('ping')
6
6
  .description('Health/version handshake against the Trawl API')
7
7
  .option('--json', 'Output as JSON')
8
8
  .action(async (opts) => {
9
- // #148 — the server route is optionalAuth (public, enriched for an admin
10
- // JWT — see the PingResponse doc above); `api.publicGet` mirrors that:
11
- // it attaches a stored/env token when one happens to be available but
12
- // never REQUIRES one, so `ping` works as a zero-config sanity check even
13
- // with no `trawl login` ever run. `api.get` (the authenticated path)
14
- // would throw notLoggedInError() locally before this ever reached the
15
- // network, defeating the whole point of a pre-login handshake.
16
9
  const data = await api.publicGet('/api/health');
17
- // #106 review (kimi) — honest exit code: a soft-degraded 200 (status !== 'ok')
18
- // must exit non-zero so `trawl ping || handle_degraded` doesn't treat a
19
- // degraded API as healthy. Applies in BOTH --json and human modes, alongside
20
- // the raw/decorated output below.
21
10
  if (data.status !== 'ok')
22
11
  process.exitCode = 1;
23
12
  if (opts.json) {
24
13
  json(data);
25
14
  return;
26
15
  }
27
- // #119 — label it as the API's version, not a bare `v0.4.0` that reads as
28
- // "the platform is v0.4" (it's the server package version field).
29
16
  const versionSuffix = data.version ? ` (api v${data.version})` : '';
30
17
  if (data.status === 'ok') {
31
18
  console.log(chalk.green('✓ OK') + versionSuffix);
32
19
  }
33
20
  else {
34
- // #106 review F5 — never green-light a degraded API just because the
35
- // HTTP call didn't throw; show the real reported status instead.
36
21
  console.log(chalk.yellow(`⚠ ${data.status}`) + versionSuffix);
37
22
  }
38
23
  });
@@ -1,138 +1,18 @@
1
1
  import { Command } from 'commander';
2
- /**
3
- * #108 — surface reorg. `run`/`list`/`get`/`data`/`history`/`run-info`/
4
- * `trigger` are promoted to top-level verbs (see index.ts's `createProgram`)
5
- * alongside `create`/`whoami`/`ping` (#114 — `create` replaced `fetch` in
6
- * that group). Each is built by an exported
7
- * `attachXCommand(parent, attachOpts)` factory instead of a fixed
8
- * `scraps.command(...)` chain, so it can be attached TWICE with a single
9
- * source-of-truth definition: once to `program` (the new canonical
10
- * top-level path) and once more, hidden, right back onto `scraps` — so every
11
- * pre-#108 `trawl scraps <verb>` invocation keeps resolving unchanged
12
- * (`{ hidden: true }` only affects help visibility, never resolution).
13
- *
14
- * Future-drift guard: ALL `.option()`/`.argument()`/`.action()` wiring for a
15
- * promoted verb MUST live INSIDE its `attachXCommand` factory body, never
16
- * bolted onto one of its two call sites (index.ts's `createProgram` for the
17
- * top-level attach, this file's own `attachXCommand(scraps, { hidden: true
18
- * })` call for the legacy one). That's the only thing keeping `trawl run
19
- * <id>` and `trawl scraps run <id>` identical — a call-site-only tweak to
20
- * one attachment would silently diverge the two.
21
- */
22
2
  type AttachOptions = {
23
3
  hidden?: boolean;
24
4
  };
25
5
  export declare const scraps: Command;
26
- /** The top-of-history snapshot pollRunProgress needs to identify which run
27
- * it's watching — see captureBeforeRunState.
28
- *
29
- * #97 — `captured` discriminates WHY `id` is undefined: `true` means the GET
30
- * succeeded and the scrap genuinely has no history yet (an honest "never
31
- * run" signal pollRunProgress can trust immediately); `false` means the GET
32
- * itself threw, so `id`/`alreadyInFlight` carry NO information at all — the
33
- * scrap could easily have prior (possibly terminal) history that this
34
- * lookup simply never saw. Before this field existed, both cases produced
35
- * the identical `{id: undefined, alreadyInFlight: false}` shape, so
36
- * pollRunProgress could not tell them apart (see its own #97 comment).
37
- */
38
6
  interface BeforeRunState {
39
7
  id?: string;
40
8
  alreadyInFlight: boolean;
41
9
  captured: boolean;
42
10
  }
43
- /** The machine-readable outcome `pollRunProgress` prints as the single final
44
- * NDJSON line under `--json` (#107 review F1). `status` is the same honest
45
- * terminal string the human-mode "Run finished: <status>" line already
46
- * shows (`success`/`error`/`empty`/`regression`/…), or `'timeout'` /
47
- * `'poll_error'` for the two non-terminal exits. */
48
11
  export interface PollOutcome {
49
12
  runId?: string;
50
13
  status: string;
51
- /** Present only for `status:'poll_error'` — the last poll failure's message. */
52
14
  error?: string;
53
15
  }
54
- /**
55
- * #91 P1 — replaces "await the run to completion, THEN open the activities
56
- * SSE stream" (which showed NOTHING: the activities SSE
57
- * (GET /api/scraps/:id/activities/stream) is backed by an in-process
58
- * EventEmitter with no backlog — trawl_node
59
- * modules/activities/services/activities.service.js — so by the time a
60
- * synchronous run has already finished there is nothing left to emit; and
61
- * the async `trigger` default enqueues the run onto a durable job queue a
62
- * SEPARATE cron-consumer pod drains, whose in-process emitter never reaches
63
- * the API pod holding the SSE connection at all).
64
- *
65
- * Instead this polls two REST reads that are BOTH Mongo-backed (not
66
- * in-process), so they work no matter which pod actually executed the run:
67
- * - GET /api/scraps/:id/activities?history=<hid>&limit=20 — the SAME
68
- * activities-list endpoint `doctor`'s fetchRunAndFix already calls
69
- * (src/commands/doctor.ts) — prints each new activity line once the new
70
- * run's history id is known.
71
- * - GET /api/scraps/:id — history[0].status/statusDetail, the SAME
72
- * terminal-status signal `lastStatus()` above already trusts (status:null
73
- * === in flight, #88 item 1) to know when the run is done.
74
- *
75
- * #93 item 1 — dedup-race fix. "Is this the run we're watching?" used to be
76
- * a single check: `history[0]._id !== beforeHistoryId`. That's wrong when
77
- * `before.alreadyInFlight` is true (a `trigger` call deduped onto a worker
78
- * job that was ALREADY pending/running at capture time): the top row IS the
79
- * run we're watching, but its `_id` never changes, so the old guard never
80
- * released and the poll ran the full timeout to a false "Timed out". The run
81
- * we're watching is now EITHER a brand-new id (fresh trigger, the common
82
- * case) OR the same id that was already in-flight (status:null) at capture
83
- * (the dedup case) — a same-id row that was already TERMINAL at capture is
84
- * neither, and must not be latched onto as "done" (it's just the previous
85
- * run, still sitting there until a genuinely new run supersedes it).
86
- *
87
- * #97 — capture-failed fallback. The dedup-race fix above assumes `before`
88
- * is trustworthy. When `captureBeforeRunState`'s own GET threw,
89
- * `before.id` is `undefined` — and that is INDISTINGUISHABLE from "the
90
- * scrap has genuinely never run" (also `id: undefined`), which is exactly
91
- * the case `isNewRun` above is designed to match on the very first row that
92
- * ever appears. So on the very first poll, ANY pre-existing history row —
93
- * even the STALE PREVIOUS run, already terminal — satisfied
94
- * `last._id !== undefined` and got reported as "the run we just launched"
95
- * finishing, when it was really just whatever ran before.
96
- *
97
- * Fix: `before.captured === false` defers trusting a baseline at all.
98
- * Instead of comparing against the (unknown) `beforeId` from the start, the
99
- * FIRST successful poll read is treated as the deferred capture itself —
100
- * exactly what `captureBeforeRunState` would have returned had its GET
101
- * succeeded — and only READS from that point on are compared against it,
102
- * via the exact same `isNewRun` / `isDedupOntoInFlight` logic above. A
103
- * pre-existing terminal row observed on that first read becomes `beforeId`
104
- * (not a match for itself), so it correctly falls into "still the stale
105
- * previous run" below and the poll keeps waiting; a row still in flight
106
- * becomes the `alreadyInFlight` baseline, exactly like a successful capture
107
- * would have recorded. Either way this costs at most one extra poll
108
- * interval, bounded by the same deadline as everything else. The one
109
- * remaining edge case — capture failed AND the scrap never ran before AND
110
- * the triggered run already finished by the very first poll — is genuinely
111
- * undecidable from "stale pre-existing row" with no more information than
112
- * this function has, so it resolves to the same honest timeout rather than
113
- * risk reporting a possibly-wrong outcome (never a lie, at worst a timeout
114
- * telling the caller to check `doctor`).
115
- *
116
- * #107 review F1 — before this fix, `run|trigger --json --watch` was
117
- * outcome-blind: `quiet` suppressed ALL output (including "Run finished:
118
- * failure" and the timeout notice), a transient poll error was caught and
119
- * silently retried FOREVER within the deadline, and the process always
120
- * exited 0 after the poll loop regardless of what the watched run actually
121
- * did — dead air, then a clean exit code, even for a failed or timed-out
122
- * run. An agent scripting this CLI had no way to tell success from failure
123
- * from "we gave up". Fixed by:
124
- * - emitting exactly ONE final NDJSON line on stdout under `--json` once
125
- * the watch reaches ANY of its three exits (terminal status, timeout, or
126
- * a persistent poll error) — `{runId,status}` (+`error` for a poll
127
- * error) — while every intermediate progress line stays suppressed
128
- * (unchanged from before);
129
- * - setting `process.exitCode` non-zero on a genuine run failure, a
130
- * timeout, or a persistent poll error, and `0` on a real success — in
131
- * BOTH `--json` and human `--watch` modes (human mode used to exit 0
132
- * unconditionally, the same bug, just silent instead of dishonest);
133
- * - giving up after `MAX_CONSECUTIVE_POLL_ERRORS` consecutive failed reads
134
- * instead of retrying the same dead endpoint for the full 300s.
135
- */
136
16
  export declare function pollRunProgress(id: string, before: BeforeRunState | undefined, opts?: {
137
17
  intervalMs?: number;
138
18
  timeoutMs?: number;