@trawlme/cli 1.18.7 → 1.19.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.
package/README.md CHANGED
@@ -83,7 +83,7 @@ trawl scraps account session set <id> -c <file>
83
83
 
84
84
  ### Claude Code skills
85
85
 
86
- The CLI bundles a Claude Code skill that teaches Claude how to use `trawl`. Once installed, Claude can manage scraps for you via prompts.
86
+ The CLI bundles 5 Claude Code skills that teach Claude how to use `trawl`. Once installed, Claude can manage scraps for you via prompts.
87
87
 
88
88
  ```
89
89
  trawl skills list List bundled skills and install status
@@ -7,6 +7,7 @@ import { promptPassword } from '../lib/prompt.js';
7
7
  import { validateObjectId } from '../lib/validate.js';
8
8
  import { classifyError, reportError, UsageError } from '../lib/errors.js';
9
9
  import { formatDoctor, formatAutofix, fetchRunAndFix, pickRun, pickFix } from './doctor.js';
10
+ import { renderPinch, pinchEnabled } from '../lib/pinch.js';
10
11
  /**
11
12
  * Print a usage/validation error consistently: human text to stderr, or a
12
13
  * machine envelope on stdout under --json (never both — reportError is the
@@ -242,6 +243,14 @@ export async function pollRunProgress(id, before, opts = {}) {
242
243
  if (last.status !== null) {
243
244
  const outcome = last.statusDetail ?? (last.status ? 'success' : 'failure');
244
245
  console.log(chalk.dim(`Run finished: ${outcome}`));
246
+ // Pinch celebrates a clean run finish (#94) — mirrors doctor.ts's own
247
+ // `status === true` success definition (regardless of statusDetail),
248
+ // never for a failed/regression run. Neither `run --watch` nor
249
+ // `trigger --watch` (the only two callers) has a --json flag, so this
250
+ // is always safe to print. Guarded by pinchEnabled() (NO_COLOR/non-TTY).
251
+ if (last.status === true && pinchEnabled()) {
252
+ console.log(renderPinch('celebrating'));
253
+ }
245
254
  return;
246
255
  }
247
256
  }
package/dist/index.d.ts CHANGED
@@ -24,7 +24,18 @@ export declare function resolveCommandName(actionCommand: Command | undefined):
24
24
  */
25
25
  export declare function collectCommandNames(root: Command): string[];
26
26
  export declare function createProgram(): Command;
27
- /** True when this module is the process entrypoint (not merely imported by a test). */
27
+ /**
28
+ * True when this module is the process entrypoint (not merely imported by a test).
29
+ *
30
+ * npm installs the global bin as a SYMLINK (`bin/trawl` -> `dist/index.js`). For an
31
+ * ESM entrypoint Node realpaths the module URL (`import.meta.url` = the real
32
+ * `dist/index.js`) but leaves `process.argv[1]` as the symlink path — so a raw
33
+ * `moduleUrl === pathToFileURL(argv1).href` compare is FALSE for the normal global
34
+ * invocation, the guard never fires, and `runCli()` never runs (silent no-op, #103).
35
+ * Canonicalise both sides with `realpathSync` before comparing. A genuine entrypoint
36
+ * was just loaded by Node, so both realpath calls resolve; the catch only trips for a
37
+ * non-file module URL (exotic loaders) — correctly "not the entrypoint".
38
+ */
28
39
  export declare function isEntryPoint(argv1: string | undefined, moduleUrl: string): boolean;
29
40
  /**
30
41
  * True when the invocation is a pure `--help`/`--version` query, a bare
@@ -36,6 +47,15 @@ export declare function isEntryPoint(argv1: string | undefined, moduleUrl: strin
36
47
  * subcommand cases)
37
48
  */
38
49
  export declare function isHelpOrVersion(argv: string[]): boolean;
50
+ /**
51
+ * True for a bare `trawl` invocation — no subcommand, no flags at all. This
52
+ * is the CLI's "first thing a new user sees" moment (commander prints its
53
+ * own top-level help right after), distinct from `isHelpOrVersion` which is
54
+ * intentionally broader (also matches `--help`/`--version`/`trawl help`
55
+ * anywhere in argv) — the Pinch wave banner (#94) only wants the narrowest
56
+ * case so it never shows up ahead of e.g. `trawl scraps --help`.
57
+ */
58
+ export declare function isBareInvocation(argv: string[]): boolean;
39
59
  /** Best-effort scan for a `--json` flag in raw argv, used only when parsing
40
60
  * itself failed before any command's own `.opts()` could be resolved (a
41
61
  * commander usage error — unknown option/command, missing required arg). Same
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, CommanderError } from 'commander';
3
- import { readFileSync } from 'node:fs';
4
- import { fileURLToPath, pathToFileURL } from 'node:url';
3
+ import { readFileSync, realpathSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { login, logout } from './commands/login.js';
7
7
  import { scraps } from './commands/scraps.js';
@@ -11,6 +11,7 @@ import { token } from './commands/token.js';
11
11
  import { autoUpdateInstalledSkills } from './lib/skills.js';
12
12
  import { initPostHog, captureCommand, shutdown, registerAllowedCommands } from './lib/posthog.js';
13
13
  import { classifyError, reportError } from './lib/errors.js';
14
+ import { renderPinch, pinchEnabled } from './lib/pinch.js';
14
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
16
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
16
17
  /**
@@ -73,9 +74,27 @@ export function createProgram() {
73
74
  program.addCommand(token);
74
75
  return program;
75
76
  }
76
- /** True when this module is the process entrypoint (not merely imported by a test). */
77
+ /**
78
+ * True when this module is the process entrypoint (not merely imported by a test).
79
+ *
80
+ * npm installs the global bin as a SYMLINK (`bin/trawl` -> `dist/index.js`). For an
81
+ * ESM entrypoint Node realpaths the module URL (`import.meta.url` = the real
82
+ * `dist/index.js`) but leaves `process.argv[1]` as the symlink path — so a raw
83
+ * `moduleUrl === pathToFileURL(argv1).href` compare is FALSE for the normal global
84
+ * invocation, the guard never fires, and `runCli()` never runs (silent no-op, #103).
85
+ * Canonicalise both sides with `realpathSync` before comparing. A genuine entrypoint
86
+ * was just loaded by Node, so both realpath calls resolve; the catch only trips for a
87
+ * non-file module URL (exotic loaders) — correctly "not the entrypoint".
88
+ */
77
89
  export function isEntryPoint(argv1, moduleUrl) {
78
- return argv1 !== undefined && moduleUrl === pathToFileURL(argv1).href;
90
+ if (argv1 === undefined)
91
+ return false;
92
+ try {
93
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(argv1);
94
+ }
95
+ catch {
96
+ return false;
97
+ }
79
98
  }
80
99
  /**
81
100
  * True when the invocation is a pure `--help`/`--version` query, a bare
@@ -96,6 +115,17 @@ export function isHelpOrVersion(argv) {
96
115
  return true;
97
116
  return false;
98
117
  }
118
+ /**
119
+ * True for a bare `trawl` invocation — no subcommand, no flags at all. This
120
+ * is the CLI's "first thing a new user sees" moment (commander prints its
121
+ * own top-level help right after), distinct from `isHelpOrVersion` which is
122
+ * intentionally broader (also matches `--help`/`--version`/`trawl help`
123
+ * anywhere in argv) — the Pinch wave banner (#94) only wants the narrowest
124
+ * case so it never shows up ahead of e.g. `trawl scraps --help`.
125
+ */
126
+ export function isBareInvocation(argv) {
127
+ return argv.slice(2).length === 0;
128
+ }
99
129
  /** Best-effort scan for a `--json` flag in raw argv, used only when parsing
100
130
  * itself failed before any command's own `.opts()` could be resolved (a
101
131
  * commander usage error — unknown option/command, missing required arg). Same
@@ -171,6 +201,12 @@ export function bestEffortCommandName(argv, allowedNames) {
171
201
  export async function runCli(argv = process.argv) {
172
202
  if (!isHelpOrVersion(argv))
173
203
  autoUpdateInstalledSkills();
204
+ // Pinch wave banner (#94) — bare `trawl` only, guarded so it never shows
205
+ // under NO_COLOR/non-TTY/piped output (pinchEnabled() covers all three).
206
+ if (isBareInvocation(argv) && pinchEnabled()) {
207
+ console.log(renderPinch('wave'));
208
+ console.log();
209
+ }
174
210
  initPostHog();
175
211
  const program = createProgram();
176
212
  // Must run before parseAsync — installs on every node in the tree,
@@ -270,6 +306,12 @@ export async function runCli(argv = process.argv) {
270
306
  // (stderr), never both; `quiet` skips the human line when the raw
271
307
  // stack was already dumped above under --debug.
272
308
  process.exitCode = reportError(err, { json: wantsJson, quiet: isDebug });
309
+ // Pinch confused frame (#94) — never under --json (stdout must stay
310
+ // pure JSON) and guarded by pinchEnabled() (NO_COLOR/non-TTY/piped).
311
+ // Printed to stderr, alongside reportError's own human line above.
312
+ if (!wantsJson && pinchEnabled()) {
313
+ console.error(renderPinch('confused'));
314
+ }
273
315
  }
274
316
  }
275
317
  finally {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Pinch — Trawl's voxel lobster mascot, rendered as 24-bit ANSI half-block
3
+ * art. Distinct from Clawd's 8-bit lane: Pinch is drawn with full 24-bit
4
+ * (`\x1b[38;2;r;g;bm` / `\x1b[48;2;r;g;bm`) color blocks, not a fixed palette.
5
+ *
6
+ * The 12×9 cube-grid (AVGRID proto v11.4, locked 2026-07-16) is packed two
7
+ * grid rows into one terminal row: the upper row's color becomes the
8
+ * half-block's foreground, the lower row's becomes its background, using the
9
+ * upper-half-block glyph '▀' (or '▄' when only the lower half is filled). A
10
+ * 9-row grid therefore renders in 5 terminal rows, 12 columns wide. '.' cells
11
+ * are transparent — no color escape is emitted for that half, so the
12
+ * terminal's own background shows through.
13
+ *
14
+ * See comes-io/trawl_cli#94.
15
+ */
16
+ export type PinchState = 'wave' | 'thinking' | 'celebrating' | 'confused';
17
+ /**
18
+ * Render Pinch as 24-bit ANSI half-block art for the given state, plus a
19
+ * one-line caption. Pure — never touches process.env/stdout; callers must
20
+ * gate on `pinchEnabled()` before printing the result.
21
+ *
22
+ * `frame` only affects 'thinking' (2-frame spinner alternation); every other
23
+ * state ignores it.
24
+ */
25
+ export declare function renderPinch(state: PinchState, frame?: number): string;
26
+ /**
27
+ * True when it's safe to print Pinch art: a real color-capable interactive
28
+ * terminal. False under NO_COLOR (https://no-color.org — presence, not
29
+ * value, disables color output), a non-TTY stdout (piped/redirected output —
30
+ * covers --json/--quiet/CI log capture), or TERM=dumb.
31
+ */
32
+ export declare function pinchEnabled(): boolean;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Pinch — Trawl's voxel lobster mascot, rendered as 24-bit ANSI half-block
3
+ * art. Distinct from Clawd's 8-bit lane: Pinch is drawn with full 24-bit
4
+ * (`\x1b[38;2;r;g;bm` / `\x1b[48;2;r;g;bm`) color blocks, not a fixed palette.
5
+ *
6
+ * The 12×9 cube-grid (AVGRID proto v11.4, locked 2026-07-16) is packed two
7
+ * grid rows into one terminal row: the upper row's color becomes the
8
+ * half-block's foreground, the lower row's becomes its background, using the
9
+ * upper-half-block glyph '▀' (or '▄' when only the lower half is filled). A
10
+ * 9-row grid therefore renders in 5 terminal rows, 12 columns wide. '.' cells
11
+ * are transparent — no color escape is emitted for that half, so the
12
+ * terminal's own background shows through.
13
+ *
14
+ * See comes-io/trawl_cli#94.
15
+ */
16
+ /** Grid-char → RGB. 'C' (cyan) is a "thinking" blip, not present in BASE_GRID. */
17
+ const PALETTE = {
18
+ B: [0x3b, 0x82, 0xf6], // blue — shell
19
+ O: [0xee, 0x90, 0x18], // orange — antennae / claws
20
+ W: [0xff, 0xff, 0xff], // white — eye whites
21
+ K: [0x12, 0x30, 0x33], // navy — pupils / mouth
22
+ P: [0xf9, 0xa8, 0xd4], // pink — claw tips
23
+ C: [0x08, 0x91, 0xb2], // cyan — thinking-spinner antenna blip
24
+ };
25
+ const TRANSPARENT = '.';
26
+ /** Base 12×9 grid — Pinch waving (also the 'wave' state, unmodified). */
27
+ const BASE_GRID = [
28
+ '...O....O...',
29
+ '...O....O...',
30
+ '.BBBBBBBBBB.',
31
+ '.BWWBBBBWWB.',
32
+ '.BWKBBBBKWB.',
33
+ '.PBBBBBBBBP.',
34
+ '.BBBKBBKBBB.',
35
+ '.BBBBKKBBBB.',
36
+ 'OO........OO',
37
+ ];
38
+ const CAPTIONS = {
39
+ wave: 'Pinch says hi.',
40
+ thinking: 'Pinch is thinking…',
41
+ celebrating: 'Pinch is celebrating!',
42
+ confused: 'Pinch looks confused.',
43
+ };
44
+ /** Replace the chars at `indices` in `row` with `ch` — never mutates `row`. */
45
+ function setCells(row, indices, ch) {
46
+ const chars = row.split('');
47
+ for (const i of indices)
48
+ chars[i] = ch;
49
+ return chars.join('');
50
+ }
51
+ /** Derive the per-state grid from BASE_GRID (which is never mutated). */
52
+ function gridForState(state, frame) {
53
+ const rows = [...BASE_GRID];
54
+ switch (state) {
55
+ case 'thinking':
56
+ // Odd frames: both antenna tips (row 0, cols 3 & 8) blip cyan — a
57
+ // 2-frame spinner alternation with no layout shift.
58
+ if (frame % 2 === 1) {
59
+ rows[0] = setCells(rows[0], [3, 8], 'C');
60
+ }
61
+ return rows;
62
+ case 'celebrating':
63
+ // Fists up: the row-8 corner claws move up to row 7's corners; row 8's
64
+ // corners go transparent (arms raised, no longer at the sides).
65
+ rows[7] = setCells(rows[7], [0, 11], 'O');
66
+ rows[8] = setCells(rows[8], [0, 1, 10, 11], TRANSPARENT);
67
+ return rows;
68
+ case 'confused':
69
+ // Pupils removed (row 4, cols 3 & 8) — blank white eyes.
70
+ rows[4] = setCells(rows[4], [3, 8], 'W');
71
+ return rows;
72
+ case 'wave':
73
+ default:
74
+ return rows;
75
+ }
76
+ }
77
+ const RESET = '\x1b[0m';
78
+ const fgCode = ([r, g, b]) => `\x1b[38;2;${r};${g};${b}m`;
79
+ const bgCode = ([r, g, b]) => `\x1b[48;2;${r};${g};${b}m`;
80
+ /**
81
+ * One terminal column: combine an upper (fg-half) + lower (bg-half) grid
82
+ * cell into a single half-block character. Either half may be transparent
83
+ * independently — no bg/fg escape is emitted for a transparent half, so the
84
+ * terminal's own background/foreground shows through it.
85
+ */
86
+ function renderCell(upper, lower) {
87
+ const upperColor = upper === TRANSPARENT ? null : PALETTE[upper];
88
+ const lowerColor = lower === TRANSPARENT ? null : PALETTE[lower];
89
+ if (!upperColor && !lowerColor)
90
+ return ' ';
91
+ // Lower half transparent — draw the upper half-block with fg only, no bg
92
+ // (the glyph's own unpainted lower half already shows the terminal bg).
93
+ if (!lowerColor)
94
+ return `${fgCode(upperColor)}▀${RESET}`;
95
+ // Upper half transparent — draw the LOWER half-block glyph instead, with
96
+ // fg set to the lower color and no bg, so the painted half is the bottom.
97
+ if (!upperColor)
98
+ return `${fgCode(lowerColor)}▄${RESET}`;
99
+ return `${fgCode(upperColor)}${bgCode(lowerColor)}▀${RESET}`;
100
+ }
101
+ /** Pack a 9-row grid into 5 terminal lines (rows 0-1, 2-3, 4-5, 6-7, 8-blank). */
102
+ function renderGrid(rows) {
103
+ const lines = [];
104
+ for (let i = 0; i < rows.length; i += 2) {
105
+ const upperRow = rows[i];
106
+ const lowerRow = rows[i + 1] ?? TRANSPARENT.repeat(upperRow.length);
107
+ let line = '';
108
+ for (let c = 0; c < upperRow.length; c++) {
109
+ line += renderCell(upperRow[c], lowerRow[c]);
110
+ }
111
+ lines.push(line);
112
+ }
113
+ return lines;
114
+ }
115
+ /**
116
+ * Render Pinch as 24-bit ANSI half-block art for the given state, plus a
117
+ * one-line caption. Pure — never touches process.env/stdout; callers must
118
+ * gate on `pinchEnabled()` before printing the result.
119
+ *
120
+ * `frame` only affects 'thinking' (2-frame spinner alternation); every other
121
+ * state ignores it.
122
+ */
123
+ export function renderPinch(state, frame = 0) {
124
+ const grid = gridForState(state, frame);
125
+ const lines = renderGrid(grid);
126
+ if (state === 'confused') {
127
+ // "beside the art" — a bold '?' to the right of the eye row (grid rows
128
+ // 4-5 pack into terminal line index 2).
129
+ lines[2] = `${lines[2]} \x1b[1m?${RESET}`;
130
+ }
131
+ return [...lines, CAPTIONS[state]].join('\n');
132
+ }
133
+ /**
134
+ * True when it's safe to print Pinch art: a real color-capable interactive
135
+ * terminal. False under NO_COLOR (https://no-color.org — presence, not
136
+ * value, disables color output), a non-TTY stdout (piped/redirected output —
137
+ * covers --json/--quiet/CI log capture), or TERM=dumb.
138
+ */
139
+ export function pinchEnabled() {
140
+ if (process.env['NO_COLOR'] !== undefined)
141
+ return false;
142
+ if (!process.stdout.isTTY)
143
+ return false;
144
+ if (process.env['TERM'] === 'dumb')
145
+ return false;
146
+ return true;
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trawlme/cli",
3
- "version": "1.18.7",
3
+ "version": "1.19.1",
4
4
  "description": "Trawl CLI — manage scraps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {