@trawlme/cli 3.12.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 +1 -1
  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/package.json +2 -2
@@ -1,39 +1,11 @@
1
- /** Reserved internal event name — never collides with a real CDP method
2
- * (every real one is `Domain.method`, always containing a dot). Emitted
3
- * once the read side of the pipe ends/errors, so any in-flight capture
4
- * can tell "Chrome went away" apart from "the browser answered". */
5
1
  const PIPE_CLOSED = '__pipe_closed__';
6
- /** A per-command deadline that never fires under normal operation. */
7
2
  const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
8
- /**
9
- * A frame Chrome sent could not be parsed as JSON (trawl_cli#183 review
10
- * finding 3). Distinguished from the generic "Chrome closed the CDP pipe"
11
- * error so a caller can tell "the peer sent garbage and was cut off" apart
12
- * from "the process actually exited" — `captureSession` reports the two
13
- * with different, factual reasons rather than defaulting a corrupt-frame
14
- * case to "Chrome exited".
15
- */
16
3
  export class CdpProtocolError extends Error {
17
4
  constructor(message) {
18
5
  super(message);
19
6
  this.name = 'CdpProtocolError';
20
7
  }
21
8
  }
22
- /**
23
- * A `send()` command's own deadline elapsed with no response (trawl_cli#183
24
- * review finding 3's fix; the gap it reopened is finding 4's bug class —
25
- * see session-capture.ts's `readCaptureDefault`). Deliberately a SIBLING of
26
- * `CdpProtocolError`, never a subclass: a protocol error means the pipe
27
- * itself can no longer be trusted, so it is torn down (`isClosed` flips
28
- * true, every OTHER pending command rejects too). A timeout means exactly
29
- * ONE command never got an answer — Chrome is still running, the pipe is
30
- * still open, every other in-flight/future command is unaffected. Making
31
- * this a `CdpProtocolError` subclass would let a caller's
32
- * `instanceof CdpProtocolError` (or a future one) treat a single slow
33
- * command as proof the whole browser is gone, which is false. Carries only
34
- * `method` and `timeoutMs` — never `params`, which can carry page-
35
- * controlled data.
36
- */
37
9
  export class CdpTimeoutError extends Error {
38
10
  method;
39
11
  timeoutMs;
@@ -61,13 +33,6 @@ export class CdpPipe {
61
33
  readStream.on('end', () => this.onClosed());
62
34
  readStream.on('close', () => this.onClosed());
63
35
  readStream.on('error', () => this.onClosed());
64
- // fd 3 (write) and fd 4 (read) are separate sockets — Chrome dying can
65
- // surface as an EPIPE on a write that lands before the read side's
66
- // 'end'/'close' ever fires. An unhandled 'error' on a Writable is a
67
- // thrown exception with no listener, which would kill the whole CLI
68
- // process and skip captureSession's `finally` (so the temp profile is
69
- // never cleaned up) — routing it through the same onClosed() path
70
- // keeps "Chrome went away" a single, always-handled state.
71
36
  writeStream.on('error', () => this.onClosed());
72
37
  }
73
38
  onClosed(err) {
@@ -83,18 +48,6 @@ export class CdpPipe {
83
48
  for (const fn of set)
84
49
  fn(undefined);
85
50
  }
86
- /**
87
- * @desc A frame that fails to parse as JSON used to be dropped silently
88
- * (`msg = undefined`, dispatch skipped) — any `send()` whose response was
89
- * that exact frame then hung forever, since nothing ever rejected it.
90
- * Now the whole pipe is torn down the same way a real Chrome exit is:
91
- * every pending command rejects (with a `CdpProtocolError`, not the
92
- * generic close message, so the cause is distinguishable), `isClosed`
93
- * flips true, and `onClose` subscribers fire — the peer sent a frame
94
- * that doesn't fit the protocol, so nothing it says next can be trusted
95
- * either. The raw frame content is never included in the error message —
96
- * it can carry page-controlled data (e.g. a `Runtime.evaluate` result).
97
- */
98
51
  onProtocolError() {
99
52
  if (this.closed)
100
53
  return;
@@ -103,11 +56,6 @@ export class CdpPipe {
103
56
  this.onClosed(err);
104
57
  }
105
58
  onData(chunk) {
106
- // Once torn down (a real close, or a corrupt frame — onProtocolError)
107
- // the read stream can still be alive and keep delivering chunks from
108
- // an untrusted peer; without this, a later VALID-looking event frame
109
- // would still `dispatch()` to any listener still registered instead
110
- // of being ignored, and `this.buffer` would keep growing forever.
111
59
  if (this.closed)
112
60
  return;
113
61
  this.buffer += chunk;
@@ -122,7 +70,7 @@ export class CdpPipe {
122
70
  }
123
71
  catch {
124
72
  this.onProtocolError();
125
- return; // torn down — whatever else is buffered is moot
73
+ return;
126
74
  }
127
75
  this.dispatch(msg);
128
76
  }
@@ -146,24 +94,6 @@ export class CdpPipe {
146
94
  fn(msg.params, msg.sessionId);
147
95
  }
148
96
  }
149
- /**
150
- * Send a CDP command and resolve with its `result`. Rejects if the pipe
151
- * closes (Chrome exited, or a corrupt frame tore it down — see
152
- * `onProtocolError`) before a response arrives, or with a
153
- * `CdpTimeoutError` if no response arrives within `timeoutMs` (defaults
154
- * to the pipe's own `commandTimeoutMs`, 30s) — the timeout names the
155
- * METHOD, never `params` (which can carry page-controlled data).
156
- * @param timeoutMs — per-call override. Lets a caller pin its own named
157
- * deadline as an assertable constant rather than relying on the pipe's
158
- * default silently matching (see session-capture.ts's
159
- * `LOCALSTORAGE_READ_TIMEOUT_MS`, which currently equals this pipe's own
160
- * 30s default but is passed explicitly anyway — a post-cap review found
161
- * that giving `Runtime.evaluate` a SHORTER override here, on the
162
- * assumption a page-blocked renderer "won't unblock itself in 30s any
163
- * more than in 5", cut off real-but-slow work that finished on its own;
164
- * there is no override value proven safe, so this parameter exists for
165
- * callers that want one, not as a recommendation to use a shorter one).
166
- */
167
97
  send(method, params = {}, sessionId, timeoutMs = this.commandTimeoutMs) {
168
98
  if (this.closed)
169
99
  return Promise.reject(this.protocolErr ?? new Error('Chrome closed the CDP pipe'));
@@ -172,10 +102,6 @@ export class CdpPipe {
172
102
  if (sessionId)
173
103
  req.sessionId = sessionId;
174
104
  return new Promise((resolve, reject) => {
175
- // Every settle path (dispatch's resolve/reject, this timeout, a
176
- // write-side EPIPE below, and onClosed's reject-all on a pipe
177
- // teardown) goes through these two wrappers, so the timer is always
178
- // cleared exactly once no matter which path wins the race.
179
105
  const settleResolve = (v) => {
180
106
  clearTimeout(timer);
181
107
  resolve(v);
@@ -197,24 +123,18 @@ export class CdpPipe {
197
123
  });
198
124
  });
199
125
  }
200
- /** Subscribe to a CDP event (`'Target.targetDestroyed'`, …). Returns an
201
- * unsubscribe function. */
202
126
  on(method, handler) {
203
127
  if (!this.listeners.has(method))
204
128
  this.listeners.set(method, new Set());
205
129
  this.listeners.get(method).add(handler);
206
130
  return () => this.listeners.get(method)?.delete(handler);
207
131
  }
208
- /** Subscribe to the pipe closing (Chrome process gone). */
209
132
  onClose(handler) {
210
133
  return this.on(PIPE_CLOSED, () => handler());
211
134
  }
212
135
  get isClosed() {
213
136
  return this.closed;
214
137
  }
215
- /** Set only when the pipe was torn down because of a corrupt frame — as
216
- * opposed to a real Chrome exit — so a caller can report the actual
217
- * cause instead of defaulting every close to "Chrome exited". */
218
138
  get protocolError() {
219
139
  return this.protocolErr;
220
140
  }
@@ -1,12 +1 @@
1
- /**
2
- * @desc Resolve the Chrome executable to launch. Precedence: an explicit
3
- * `--chrome <path>` flag (validated by the caller) > `TRAWL_CHROME_PATH` >
4
- * `PUPPETEER_EXECUTABLE_PATH`/`CHROME_PATH` (common conventions other
5
- * tools already set) > the well-known per-OS install paths. Returns null
6
- * when nothing is found — the caller is responsible for a factual,
7
- * non-guessing error message.
8
- * @param {NodeJS.ProcessEnv} env
9
- * @param {NodeJS.Platform} platform
10
- * @param {(path: string) => boolean} exists — injectable for tests.
11
- */
12
1
  export declare function findChrome(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, exists?: (path: string) => boolean): string | null;
@@ -1,11 +1,3 @@
1
- /**
2
- * Locate a system Chrome/Chromium executable (trawl_cli#183). Same
3
- * discovery approach as `@trawlme/skills`'s
4
- * `trawl-scrap-local-test/scripts/run-local.mjs` `findChrome()` — a short
5
- * list of well-known install paths per OS, first match wins — extended
6
- * with an env override and a Windows list (the skill script only needed
7
- * mac/Linux for a dev-machine local test; this ships to every user's OS).
8
- */
9
1
  import { existsSync } from 'node:fs';
10
2
  const CANDIDATES = {
11
3
  darwin: [
@@ -27,17 +19,6 @@ const CANDIDATES = {
27
19
  'C:\\Program Files\\Chromium\\Application\\chrome.exe',
28
20
  ],
29
21
  };
30
- /**
31
- * @desc Resolve the Chrome executable to launch. Precedence: an explicit
32
- * `--chrome <path>` flag (validated by the caller) > `TRAWL_CHROME_PATH` >
33
- * `PUPPETEER_EXECUTABLE_PATH`/`CHROME_PATH` (common conventions other
34
- * tools already set) > the well-known per-OS install paths. Returns null
35
- * when nothing is found — the caller is responsible for a factual,
36
- * non-guessing error message.
37
- * @param {NodeJS.ProcessEnv} env
38
- * @param {NodeJS.Platform} platform
39
- * @param {(path: string) => boolean} exists — injectable for tests.
40
- */
41
22
  export function findChrome(env = process.env, platform = process.platform, exists = existsSync) {
42
23
  const envCandidates = [env.TRAWL_CHROME_PATH, env.PUPPETEER_EXECUTABLE_PATH, env.CHROME_PATH].filter((p) => typeof p === 'string' && p.length > 0);
43
24
  for (const p of envCandidates) {
@@ -1,48 +1,8 @@
1
- /**
2
- * Spawn a headed, isolated Chrome instance wired for raw CDP over
3
- * `--remote-debugging-pipe` (trawl_cli#183). Thin glue only — the actual
4
- * protocol lives in cdp-pipe.ts, kept separate so this file's job (build
5
- * the right flags, wire fd 3/4, fail loudly if the platform doesn't expose
6
- * them) stays small enough to unit test with a mocked `spawn`.
7
- */
8
1
  import { spawn, type ChildProcess } from 'node:child_process';
9
2
  import { CdpPipe } from './cdp-pipe.js';
10
3
  export interface LaunchedChrome {
11
4
  proc: ChildProcess;
12
5
  cdp: CdpPipe;
13
6
  }
14
- /**
15
- * @desc The exact CLI flags Chrome is launched with. Pure + exported so
16
- * the flag set is testable without spawning a real process.
17
- * - `--remote-debugging-pipe` — CDP over fd 3 (in) / fd 4 (out), the
18
- * zero-dependency transport this command relies on.
19
- * - `--user-data-dir` — an ephemeral, isolated profile (never the user's
20
- * real Chrome profile/cookies).
21
- * - `--no-first-run` / `--no-default-browser-check` — skip first-run UI
22
- * that would otherwise sit in front of the target URL.
23
- * - `--disable-sync` — never touches the user's real Google account sync.
24
- * - `--password-store=basic` / `--use-mock-keychain` — a fresh profile
25
- * would otherwise prompt for the macOS login keychain (Safe Storage) the
26
- * first time Chrome touches its cookie/password store; these keep the
27
- * whole flow non-blocking on an ephemeral profile that's deleted right
28
- * after (mirrors Crawl4AI's `crwl profiles`, cited in the issue).
29
- * - `--new-window` — always its own window, never a tab folded into an
30
- * already-running Chrome instance.
31
- * - No `--no-sandbox`: this runs headed on the user's own machine, not in
32
- * a locked-down CI container — the sandbox stays on.
33
- */
34
7
  export declare function buildChromeArgs(userDataDir: string, targetUrl: string): string[];
35
- /**
36
- * @desc Launch Chrome and wire a CdpPipe to its fd 3 (write) / fd 4 (read).
37
- * Returns a Promise rather than the `ChildProcess` synchronously: `spawn()`
38
- * can fail AFTER returning — EACCES on a non-executable path, an ENOENT
39
- * race, EPERM, E2BIG — by emitting an `'error'` event rather than throwing,
40
- * and that event is attached to BEFORE the fd 3/4 check below so a failure
41
- * from either cause rejects this promise instead of crashing the process
42
- * (an EventEmitter with zero `'error'` listeners throws the error itself).
43
- * @rejects {Error} if `spawn()` itself failed (the original error, so
44
- * `.code` like `EACCES` survives), or if the platform/Chrome build didn't
45
- * expose fd 3/4 as pipes (e.g. `--remote-debugging-pipe` unsupported) — a
46
- * factual message, never a silent hang.
47
- */
48
8
  export declare function launchChrome(executablePath: string, targetUrl: string, userDataDir: string, spawnFn?: typeof spawn): Promise<LaunchedChrome>;
@@ -1,32 +1,5 @@
1
- /**
2
- * Spawn a headed, isolated Chrome instance wired for raw CDP over
3
- * `--remote-debugging-pipe` (trawl_cli#183). Thin glue only — the actual
4
- * protocol lives in cdp-pipe.ts, kept separate so this file's job (build
5
- * the right flags, wire fd 3/4, fail loudly if the platform doesn't expose
6
- * them) stays small enough to unit test with a mocked `spawn`.
7
- */
8
1
  import { spawn } from 'node:child_process';
9
2
  import { CdpPipe } from './cdp-pipe.js';
10
- /**
11
- * @desc The exact CLI flags Chrome is launched with. Pure + exported so
12
- * the flag set is testable without spawning a real process.
13
- * - `--remote-debugging-pipe` — CDP over fd 3 (in) / fd 4 (out), the
14
- * zero-dependency transport this command relies on.
15
- * - `--user-data-dir` — an ephemeral, isolated profile (never the user's
16
- * real Chrome profile/cookies).
17
- * - `--no-first-run` / `--no-default-browser-check` — skip first-run UI
18
- * that would otherwise sit in front of the target URL.
19
- * - `--disable-sync` — never touches the user's real Google account sync.
20
- * - `--password-store=basic` / `--use-mock-keychain` — a fresh profile
21
- * would otherwise prompt for the macOS login keychain (Safe Storage) the
22
- * first time Chrome touches its cookie/password store; these keep the
23
- * whole flow non-blocking on an ephemeral profile that's deleted right
24
- * after (mirrors Crawl4AI's `crwl profiles`, cited in the issue).
25
- * - `--new-window` — always its own window, never a tab folded into an
26
- * already-running Chrome instance.
27
- * - No `--no-sandbox`: this runs headed on the user's own machine, not in
28
- * a locked-down CI container — the sandbox stays on.
29
- */
30
3
  export function buildChromeArgs(userDataDir, targetUrl) {
31
4
  return [
32
5
  '--remote-debugging-pipe',
@@ -40,48 +13,13 @@ export function buildChromeArgs(userDataDir, targetUrl) {
40
13
  targetUrl,
41
14
  ];
42
15
  }
43
- /**
44
- * @desc Launch Chrome and wire a CdpPipe to its fd 3 (write) / fd 4 (read).
45
- * Returns a Promise rather than the `ChildProcess` synchronously: `spawn()`
46
- * can fail AFTER returning — EACCES on a non-executable path, an ENOENT
47
- * race, EPERM, E2BIG — by emitting an `'error'` event rather than throwing,
48
- * and that event is attached to BEFORE the fd 3/4 check below so a failure
49
- * from either cause rejects this promise instead of crashing the process
50
- * (an EventEmitter with zero `'error'` listeners throws the error itself).
51
- * @rejects {Error} if `spawn()` itself failed (the original error, so
52
- * `.code` like `EACCES` survives), or if the platform/Chrome build didn't
53
- * expose fd 3/4 as pipes (e.g. `--remote-debugging-pipe` unsupported) — a
54
- * factual message, never a silent hang.
55
- */
56
16
  export function launchChrome(executablePath, targetUrl, userDataDir, spawnFn = spawn) {
57
17
  return new Promise((resolve, reject) => {
58
- // stderr is 'ignore', not 'pipe': nothing here ever reads it (unlike the
59
- // rejected `ws`-over-port design, `--remote-debugging-pipe` needs none of
60
- // Chrome's stderr output), and a 'pipe' nobody drains fills its OS pipe
61
- // buffer — Chrome's own logging would then block on write mid-session,
62
- // freezing the browser the human is mid-login in.
63
18
  const proc = spawnFn(executablePath, buildChromeArgs(userDataDir, targetUrl), {
64
19
  stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'],
65
- // POSIX only: makes Chrome the leader of its OWN process group, so its
66
- // helper subprocesses (renderer/GPU/network service — Chrome forks
67
- // several even for one window) live in that group too, distinct from
68
- // ours. Verified against a real launch (trawl_cli#183's loopback E2E):
69
- // without this, `proc.kill('SIGKILL')` only killed the main Chrome
70
- // process — its helpers briefly kept the user-data-dir's files open,
71
- // and `rmSync` silently lost that race, leaking the temp profile on
72
- // every run. session-capture.ts's cleanup kills `-proc.pid` (the whole
73
- // group) instead of `proc.pid` alone, which requires this.
74
20
  detached: process.platform !== 'win32',
75
21
  });
76
22
  let settled = false;
77
- // Attached as the very first thing after `spawnFn` returns — before the
78
- // fd 3/4 check below, and before anything here ever awaits — so nothing
79
- // can slip through unobserved. Stays attached FOREVER, including after
80
- // this promise settles: a LATE error (spawn succeeded, then something
81
- // else goes wrong long after this promise already resolved) must not
82
- // crash the process either, and once `settled` is true this listener is
83
- // a permanent no-op — whatever consumes `proc` from here on notices
84
- // Chrome went away some other way (the CDP pipe closing).
85
23
  proc.on('error', (err) => {
86
24
  if (settled)
87
25
  return;
@@ -90,7 +28,6 @@ export function launchChrome(executablePath, targetUrl, userDataDir, spawnFn = s
90
28
  proc.kill('SIGKILL');
91
29
  }
92
30
  catch {
93
- // already gone
94
31
  }
95
32
  reject(err);
96
33
  });
@@ -102,16 +39,10 @@ export function launchChrome(executablePath, targetUrl, userDataDir, spawnFn = s
102
39
  proc.kill('SIGKILL');
103
40
  }
104
41
  catch {
105
- // already gone
106
42
  }
107
43
  reject(new Error('Chrome did not expose the CDP pipe (fd 3/4) — this Chrome build or platform may not support --remote-debugging-pipe.'));
108
44
  return;
109
45
  }
110
- // Only declare success once the OS confirms the process actually
111
- // started — the documented signal for that ('spawn', not merely
112
- // "spawn() returned without throwing") — rather than assuming
113
- // immediately: that's exactly the assumption the bug this fixes was
114
- // built on.
115
46
  proc.once('spawn', () => {
116
47
  if (settled)
117
48
  return;
@@ -4,67 +4,14 @@ interface TrawlConfig {
4
4
  token: string;
5
5
  telemetry: boolean;
6
6
  telemetryUserId: string;
7
- /** Opt-out switch for the passive "update available" notifier
8
- * (src/lib/updateNotifier.ts). Optional — absent/undefined means enabled;
9
- * only an explicit `false` disables it. No default entry needed since it's
10
- * optional. (#129) */
11
7
  updateNotifier?: boolean;
12
- /** Opt-out switch for the throttled post-run referral tip (lib/tips.ts).
13
- * Same optional/absent-means-enabled shape as `updateNotifier` above. */
14
8
  tips?: boolean;
15
9
  referralTipShownAt: number;
16
10
  skillsNudgeShownAt: number;
17
11
  }
18
12
  declare const config: Conf<TrawlConfig>;
19
- /**
20
- * Resolve the effective API base URL.
21
- * Precedence: TRAWL_API_URL env > stored `trawl login --url` > default.
22
- * Mirrors the TRAWL_TOKEN / TRAWL_TELEMETRY session-override pattern so scripts
23
- * (e.g. infra /trawl-prod-qa against https://dev.trawl.me) can retarget the CLI
24
- * without mutating the operator's persisted config. (#56)
25
- */
26
13
  export declare function getApiUrl(): string;
27
- /**
28
- * Resolve the effective credential — a scoped API key or a session JWT.
29
- * Precedence: TRAWL_API_KEY env > TRAWL_TOKEN env > stored `trawl login`
30
- * token. TRAWL_API_KEY resolves first so an agent harness that carries BOTH
31
- * (e.g. a scoped key set globally alongside a leftover human TRAWL_TOKEN)
32
- * gets the key unambiguously, never a silent fall-through to the
33
- * higher-privilege JWT. Lets CI/agents authenticate headlessly
34
- * (`TRAWL_API_KEY=trawl_xxx trawl list` or `TRAWL_TOKEN=<jwt> trawl list`)
35
- * without ever touching the on-disk config — and without a stored token
36
- * being silently sent to whatever TRAWL_API_URL points at instead (cross-env
37
- * credential misuse). Every request/upload/publicGet/getText/stream call
38
- * site in api.ts must read the token through this, never through
39
- * `config.get('token')` directly. Mirrors getApiUrl(). (#68, #169)
40
- */
41
14
  export declare function getToken(): string;
42
- /**
43
- * Discriminate which of the two credential shapes this CLI can send a
44
- * `token` is: a scoped API key (`Authorization: Bearer`) or a session JWT
45
- * (`Cookie: TOKEN=`). The `trawl_` prefix is the SERVER's own test —
46
- * trawl_node's `authenticateApiKey.js` checks `rawKey.startsWith('trawl_')`
47
- * — not a convention invented on this side; this function exists so the two
48
- * sides can never quietly drift apart on what counts as a key. Defaults to
49
- * the live `getToken()` result so most callers (e.g. `whoami`'s JWT-only
50
- * guard) need no argument; `api.ts`'s `authHeaders()` instead passes the
51
- * exact token it already resolved for THIS request, so a request's headers
52
- * and its classification of that same token can never disagree. (#169)
53
- */
54
15
  export declare function getAuthMode(token?: string): 'apiKey' | 'jwt';
55
- /**
56
- * Which of getToken()'s two env-var overrides actually won, if either —
57
- * mirrors its TRAWL_API_KEY > TRAWL_TOKEN precedence exactly. Neither
58
- * `trawl login` nor `trawl logout` can unset a caller's OWN environment, so
59
- * whichever of these is set keeps overriding the stored config token
60
- * regardless of what those commands just did (#169 review). login.ts's
61
- * post-login/post-logout warnings and api.ts's apiKey-mode auth-failure
62
- * messages/`next` steps all need the SAME answer to "what has to be unset
63
- * before `trawl login` takes effect" — resolved once here rather than each
64
- * call site re-deriving its own copy that could quietly drift from
65
- * getToken()'s actual precedence. Returns null when the credential came from
66
- * the stored config token instead — nothing to unset there, `trawl login`
67
- * alone already fixes that case.
68
- */
69
16
  export declare function getLiveAuthEnvVar(): 'TRAWL_API_KEY' | 'TRAWL_TOKEN' | null;
70
17
  export default config;
@@ -1,12 +1,4 @@
1
1
  import Conf from 'conf';
2
- /**
3
- * TRAWL_CONFIG_DIR overrides where Conf stores the config file (its `cwd`
4
- * option). Without this, the CLI has zero config isolation on macOS — Conf's
5
- * env-paths dependency hardcodes `~/Library/Preferences/...` and ignores
6
- * XDG_CONFIG_HOME — so CI/agent runs and concurrent `trawl login`s race on
7
- * one shared on-disk file. Point at an ephemeral dir for hermetic runs, e.g.
8
- * `TRAWL_CONFIG_DIR=$(mktemp -d) trawl login --token …`. Rescope of #59. (#68)
9
- */
10
2
  const configDir = process.env['TRAWL_CONFIG_DIR']?.trim();
11
3
  const config = new Conf({
12
4
  projectName: 'trawl-cli',
@@ -20,31 +12,10 @@ const config = new Conf({
20
12
  skillsNudgeShownAt: 0,
21
13
  },
22
14
  });
23
- /**
24
- * Resolve the effective API base URL.
25
- * Precedence: TRAWL_API_URL env > stored `trawl login --url` > default.
26
- * Mirrors the TRAWL_TOKEN / TRAWL_TELEMETRY session-override pattern so scripts
27
- * (e.g. infra /trawl-prod-qa against https://dev.trawl.me) can retarget the CLI
28
- * without mutating the operator's persisted config. (#56)
29
- */
30
15
  export function getApiUrl() {
31
16
  const override = process.env['TRAWL_API_URL']?.trim();
32
17
  return override ? override : config.get('apiUrl');
33
18
  }
34
- /**
35
- * Resolve the effective credential — a scoped API key or a session JWT.
36
- * Precedence: TRAWL_API_KEY env > TRAWL_TOKEN env > stored `trawl login`
37
- * token. TRAWL_API_KEY resolves first so an agent harness that carries BOTH
38
- * (e.g. a scoped key set globally alongside a leftover human TRAWL_TOKEN)
39
- * gets the key unambiguously, never a silent fall-through to the
40
- * higher-privilege JWT. Lets CI/agents authenticate headlessly
41
- * (`TRAWL_API_KEY=trawl_xxx trawl list` or `TRAWL_TOKEN=<jwt> trawl list`)
42
- * without ever touching the on-disk config — and without a stored token
43
- * being silently sent to whatever TRAWL_API_URL points at instead (cross-env
44
- * credential misuse). Every request/upload/publicGet/getText/stream call
45
- * site in api.ts must read the token through this, never through
46
- * `config.get('token')` directly. Mirrors getApiUrl(). (#68, #169)
47
- */
48
19
  export function getToken() {
49
20
  const apiKey = process.env['TRAWL_API_KEY']?.trim();
50
21
  if (apiKey)
@@ -52,35 +23,9 @@ export function getToken() {
52
23
  const override = process.env['TRAWL_TOKEN']?.trim();
53
24
  return override ? override : config.get('token');
54
25
  }
55
- /**
56
- * Discriminate which of the two credential shapes this CLI can send a
57
- * `token` is: a scoped API key (`Authorization: Bearer`) or a session JWT
58
- * (`Cookie: TOKEN=`). The `trawl_` prefix is the SERVER's own test —
59
- * trawl_node's `authenticateApiKey.js` checks `rawKey.startsWith('trawl_')`
60
- * — not a convention invented on this side; this function exists so the two
61
- * sides can never quietly drift apart on what counts as a key. Defaults to
62
- * the live `getToken()` result so most callers (e.g. `whoami`'s JWT-only
63
- * guard) need no argument; `api.ts`'s `authHeaders()` instead passes the
64
- * exact token it already resolved for THIS request, so a request's headers
65
- * and its classification of that same token can never disagree. (#169)
66
- */
67
26
  export function getAuthMode(token = getToken()) {
68
27
  return token.startsWith('trawl_') ? 'apiKey' : 'jwt';
69
28
  }
70
- /**
71
- * Which of getToken()'s two env-var overrides actually won, if either —
72
- * mirrors its TRAWL_API_KEY > TRAWL_TOKEN precedence exactly. Neither
73
- * `trawl login` nor `trawl logout` can unset a caller's OWN environment, so
74
- * whichever of these is set keeps overriding the stored config token
75
- * regardless of what those commands just did (#169 review). login.ts's
76
- * post-login/post-logout warnings and api.ts's apiKey-mode auth-failure
77
- * messages/`next` steps all need the SAME answer to "what has to be unset
78
- * before `trawl login` takes effect" — resolved once here rather than each
79
- * call site re-deriving its own copy that could quietly drift from
80
- * getToken()'s actual precedence. Returns null when the credential came from
81
- * the stored config token instead — nothing to unset there, `trawl login`
82
- * alone already fixes that case.
83
- */
84
29
  export function getLiveAuthEnvVar() {
85
30
  if (process.env['TRAWL_API_KEY']?.trim())
86
31
  return 'TRAWL_API_KEY';
@@ -1,70 +1,15 @@
1
- /**
2
- * #107 — the machine contract's non-interactive rule, in one place. True only
3
- * when a blocking interactive prompt is safe to show: NOT `--json` (a machine
4
- * consumer needs pure stdout, and there's no human reading a prompt anyway)
5
- * AND both stdin/stdout are real TTYs. A pipe/redirect/CI runner — including
6
- * an agent driving this CLI as a subprocess — has nowhere for a human to type
7
- * an answer; a blocking `readline` prompt in that situation hangs forever
8
- * instead of ever returning.
9
- */
10
1
  export declare function isInteractive(opts?: {
11
2
  json?: boolean;
12
3
  }): boolean;
13
4
  export interface ConfirmOutcome {
14
- /** True when the caller should proceed with the guarded action. */
15
5
  proceed: boolean;
16
- /**
17
- * True when refused because the invocation is non-interactive (`--json`,
18
- * or stdin/stdout isn't a real TTY) — a structured usage error has ALREADY
19
- * been reported (the stderr line and/or the `--json` envelope) and
20
- * `process.exitCode` set to 2. The caller must return immediately without
21
- * printing anything else (mirrors every other `usageError()` call site).
22
- */
23
6
  blocked: boolean;
24
7
  }
25
- /**
26
- * Shared guard for every destructive y/N confirmation in the CLI (`scraps
27
- * delete`, `scraps account delete`, …). #107 — before this, each call site
28
- * hand-rolled its own `readline` question with no non-interactive escape
29
- * hatch, so a scripted/agent invocation with no `-f`/`--force` would hang
30
- * forever waiting for a y/N answer that could never arrive.
31
- *
32
- * `-f`/`--force` (`opts.force`) always pre-confirms, interactive or not — the
33
- * caller already gave explicit consent up front. Otherwise:
34
- * - Non-interactive (`--json`, or stdin/stdout isn't a real TTY): NEVER
35
- * prompts. Reports a structured usage error (exit 2) instead, via the same
36
- * central `reportError` formatting path every other error in this CLI
37
- * uses (stderr human line, or the `--json` envelope on stdout).
38
- * - Interactive: prompts via `readline` exactly like the pre-#107 call
39
- * sites did.
40
- *
41
- * `message` MUST be plain, unstyled text — it feeds both the refusal
42
- * UsageError's message (human stderr line AND the `--json` error envelope on
43
- * stdout) and the fallback prompt text. Before this, call sites passed a
44
- * `chalk.bold(id)`-styled string as `message`, which leaked raw ANSI escape
45
- * bytes into the `--json` envelope (e.g. `scraps delete X --json` on the
46
- * refusal path emitted a message with the raw bold-on/off escape sequence
47
- * wrapped around the id) — unusable for a script/agent parsing that string.
48
- * `promptMessage` is the OPTIONAL styled variant shown only for the
49
- * interactive TTY `[y/N]` prompt (chalk is safe there — no machine ever
50
- * reads it); it defaults to `message` when omitted. (#107 review F2)
51
- */
52
8
  export declare function confirmDestructive(message: string, opts?: {
53
9
  force?: boolean;
54
10
  json?: boolean;
55
11
  promptMessage?: string;
56
12
  }): Promise<ConfirmOutcome>;
57
- /**
58
- * Guard for a required interactive value that has no flag-supplied value yet
59
- * (login's email prompt, …) — for call sites that propagate errors by
60
- * `throw`ing and let the top-level handler in index.ts classify + report
61
- * (login.ts's existing convention), as opposed to `scraps.ts`'s local
62
- * exitCode-setting `usageError()` style (which should check `isInteractive`
63
- * directly instead of using this). Same non-interactive contract as
64
- * `confirmDestructive`: never invokes a blocking prompt when `--json` is set
65
- * or stdin/stdout isn't a real TTY — throws a `UsageError` naming the flag to
66
- * pass instead. (#107)
67
- */
68
13
  export declare function requireInteractive(message: string, opts?: {
69
14
  json?: boolean;
70
15
  }): void;
@@ -1,47 +1,11 @@
1
1
  import chalk from 'chalk';
2
2
  import { UsageError } from './errors.js';
3
3
  import { reportError } from './errors.js';
4
- /**
5
- * #107 — the machine contract's non-interactive rule, in one place. True only
6
- * when a blocking interactive prompt is safe to show: NOT `--json` (a machine
7
- * consumer needs pure stdout, and there's no human reading a prompt anyway)
8
- * AND both stdin/stdout are real TTYs. A pipe/redirect/CI runner — including
9
- * an agent driving this CLI as a subprocess — has nowhere for a human to type
10
- * an answer; a blocking `readline` prompt in that situation hangs forever
11
- * instead of ever returning.
12
- */
13
4
  export function isInteractive(opts = {}) {
14
5
  if (opts.json)
15
6
  return false;
16
7
  return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
17
8
  }
18
- /**
19
- * Shared guard for every destructive y/N confirmation in the CLI (`scraps
20
- * delete`, `scraps account delete`, …). #107 — before this, each call site
21
- * hand-rolled its own `readline` question with no non-interactive escape
22
- * hatch, so a scripted/agent invocation with no `-f`/`--force` would hang
23
- * forever waiting for a y/N answer that could never arrive.
24
- *
25
- * `-f`/`--force` (`opts.force`) always pre-confirms, interactive or not — the
26
- * caller already gave explicit consent up front. Otherwise:
27
- * - Non-interactive (`--json`, or stdin/stdout isn't a real TTY): NEVER
28
- * prompts. Reports a structured usage error (exit 2) instead, via the same
29
- * central `reportError` formatting path every other error in this CLI
30
- * uses (stderr human line, or the `--json` envelope on stdout).
31
- * - Interactive: prompts via `readline` exactly like the pre-#107 call
32
- * sites did.
33
- *
34
- * `message` MUST be plain, unstyled text — it feeds both the refusal
35
- * UsageError's message (human stderr line AND the `--json` error envelope on
36
- * stdout) and the fallback prompt text. Before this, call sites passed a
37
- * `chalk.bold(id)`-styled string as `message`, which leaked raw ANSI escape
38
- * bytes into the `--json` envelope (e.g. `scraps delete X --json` on the
39
- * refusal path emitted a message with the raw bold-on/off escape sequence
40
- * wrapped around the id) — unusable for a script/agent parsing that string.
41
- * `promptMessage` is the OPTIONAL styled variant shown only for the
42
- * interactive TTY `[y/N]` prompt (chalk is safe there — no machine ever
43
- * reads it); it defaults to `message` when omitted. (#107 review F2)
44
- */
45
9
  export async function confirmDestructive(message, opts = {}) {
46
10
  if (opts.force)
47
11
  return { proceed: true, blocked: false };
@@ -61,17 +25,6 @@ export async function confirmDestructive(message, opts = {}) {
61
25
  rl.close();
62
26
  }
63
27
  }
64
- /**
65
- * Guard for a required interactive value that has no flag-supplied value yet
66
- * (login's email prompt, …) — for call sites that propagate errors by
67
- * `throw`ing and let the top-level handler in index.ts classify + report
68
- * (login.ts's existing convention), as opposed to `scraps.ts`'s local
69
- * exitCode-setting `usageError()` style (which should check `isInteractive`
70
- * directly instead of using this). Same non-interactive contract as
71
- * `confirmDestructive`: never invokes a blocking prompt when `--json` is set
72
- * or stdin/stdout isn't a real TTY — throws a `UsageError` naming the flag to
73
- * pass instead. (#107)
74
- */
75
28
  export function requireInteractive(message, opts = {}) {
76
29
  if (!isInteractive(opts)) {
77
30
  throw new UsageError(message);