@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 +1 -1
- package/dist/commands/scraps.js +9 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.js +46 -4
- package/dist/lib/pinch.d.ts +32 -0
- package/dist/lib/pinch.js +147 -0
- package/package.json +1 -1
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
|
|
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
|
package/dist/commands/scraps.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
+
}
|