@trawlme/cli 2.2.0 → 2.4.0

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
@@ -54,7 +54,7 @@ trawl ping [--json] Health/version handshake against
54
54
 
55
55
  > **No breaking change:** every verb above is also still reachable under its pre-reorg path, `trawl scraps <verb>` (e.g. `trawl scraps list`, `trawl scraps run <id>`) — kept as a hidden alias so scripts written before the surface reorg keep working. `trawl --help` only shows the top-level form above; `trawl scraps --help` only shows the remaining scrap-management commands below.
56
56
 
57
- `create`/`whoami`/`ping` are fully non-interactive — all three read auth only from `TRAWL_TOKEN`/the stored login token, never prompt. `trawl create` runs the AI wizard server-side (`POST /api/ai/wizard`): generate scrap code from `--prompt` via LLM, persist the scrap, trigger its FIRST run, and auto-fix on failure (default on — `--no-autofix` disables it, sending `autoFix:false`). `success` is an honest outcome of that first run, not "did the HTTP call succeed" — a failed first run is still a 200 response (the scrap was still created; auto-fix, when enabled, retries in the background), and the CLI exits 1 in that case (both human and `--json` modes) even though `--json` always prints the raw payload verbatim. The call legitimately takes 30–250s+ server-side (AI generation + a real run), same long-run timeout as `run`/`data --fresh`/`trigger --wait` below. `trawl whoami`/`trawl ping` mirror the MCP `trawl_whoami`/`trawl_health_ping` tools as closely as the REST surface allows (`GET /api/users/me` / `GET /api/health`) — `ping`'s `--json` payload is admin-enriched (version/uptime/db) and just `{"status":"ok"}` for anyone else.
57
+ `create`/`whoami`/`ping` are fully non-interactive — all three read auth only from `TRAWL_TOKEN`/the stored login token, never prompt. `trawl create` runs the AI wizard server-side (`POST /api/ai/wizard`): generate scrap code from `--prompt` via LLM, persist the scrap, trigger its FIRST run, and auto-fix on failure (default on — `--no-autofix` disables it, sending `autoFix:false`). On a successful first run (human mode) it prints a small **data sample** (item count + first-item fields + one truncated value) as proof of value — best-effort, silent if the sample can't be fetched — and points `Next step` at `trawl data <id>` (the data), with `trawl get <id>` as the secondary detail view. `--json` skips the sample fetch and prints the raw wizard payload verbatim. `success` is an honest outcome of that first run, not "did the HTTP call succeed" — a failed first run is still a 200 response (the scrap was still created; auto-fix, when enabled, retries in the background), and the CLI exits 1 in that case (both human and `--json` modes) even though `--json` always prints the raw payload verbatim. The call legitimately takes 30–250s+ server-side (AI generation + a real run), same long-run timeout as `run`/`data --fresh`/`trigger --wait` below. `trawl whoami`/`trawl ping` mirror the MCP `trawl_whoami`/`trawl_health_ping` tools as closely as the REST surface allows (`GET /api/users/me` / `GET /api/health`) — `ping`'s `--json` payload is admin-enriched (version/uptime/db) and just `{"status":"ok"}` for anyone else.
58
58
 
59
59
  > **`create` is NOT idempotent, and every wizard-created scrap runs on a DAILY cron by default.** A client-side timeout (exit `5`, a `NetworkError`) does not mean the wizard failed server-side — scrap creation + the first run keep going after the CLI gives up waiting, so the scrap may already exist. Run `trawl list` and look for a matching URL/title **before** retrying — a blind retry creates a DUPLICATE scrap and burns AI-generation quota a second time for the same goal. Separately, the scrap the wizard creates is scheduled to re-run every day at 07:00 UTC (`cron: "0 7 * * *"`, hardcoded server-side, unrelated to `--no-autofix`) — each of those recurring runs consumes execute quota like any other run. Review the generated scrap, then change or disable the schedule with `trawl update <id> --cron <expr>` (or `--no-cron` to disable it). Because the call can legitimately run 250s+, also confirm `TRAWL_TIMEOUT` isn't set to something tighter than `create` needs — the env var always wins over `create`'s own 300s default (see [Environment variables](#environment-variables)), so a value set for another purpose (e.g. a tight CI smoke-test budget) silently clamps `create` too; unset it or raise it before running `create`.
60
60
 
@@ -5,6 +5,7 @@ import { api, LONG_RUN_TIMEOUT_MS, NetworkError } from '../lib/api.js';
5
5
  import { json } from '../lib/format.js';
6
6
  import { requireUrl, requireString } from '../lib/validate.js';
7
7
  import { UsageError } from '../lib/errors.js';
8
+ import { renderPinch, pinchEnabled } from '../lib/pinch.js';
8
9
  /** Best-effort, honest first-run summary — never claims a background retry
9
10
  * happened when auto-fix was disabled for this call, and never claims a
10
11
  * scrap was persisted when the response carries none (#114-F3 — a hard
@@ -50,6 +51,41 @@ function scheduleLabel(scrap) {
50
51
  }
51
52
  return 'scheduled daily by default';
52
53
  }
54
+ /**
55
+ * #121 — onboarding principle: a successful `create` should SHOW the value,
56
+ * not just an id. Best-effort fetch of the first run's persisted data (the
57
+ * same read-only, no-quota path `trawl data <id>` uses:
58
+ * `GET /api/historys/:historyId` → a JSON string `{ data: [...] }`) and print
59
+ * a small proof-of-value sample (count + first-item keys + one truncated
60
+ * value line — never a full dump; that's what `trawl data --json` is for).
61
+ * ANY failure (network, parse, no data) is swallowed silently — the sample is
62
+ * a bonus, it must never turn a successful create into a failure or noise.
63
+ */
64
+ async function printDataSample(historyId) {
65
+ try {
66
+ const detail = await api.get(`/api/historys/${historyId}`);
67
+ if (typeof detail?.data !== 'string' || !detail.data)
68
+ return;
69
+ const items = JSON.parse(detail.data)?.data;
70
+ if (!Array.isArray(items) || items.length === 0)
71
+ return;
72
+ console.log(chalk.dim(` Sample: `) + `${items.length} item${items.length === 1 ? '' : 's'}`);
73
+ const first = items[0];
74
+ if (first && typeof first === 'object') {
75
+ const keys = Object.keys(first);
76
+ console.log(chalk.dim(` Fields: `) + keys.join(', '));
77
+ const firstKey = keys[0];
78
+ if (firstKey) {
79
+ const raw = String(first[firstKey] ?? '');
80
+ const val = raw.length > 60 ? `${raw.slice(0, 60)}…` : raw;
81
+ console.log(chalk.dim(` ${firstKey.slice(0, 10).padEnd(10)}: `) + val);
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ // best-effort — a missing sample never fails or noises up a good create
87
+ }
88
+ }
53
89
  export const create = new Command('create')
54
90
  .description('Create a persistent, self-healing scrap from a URL + a goal (AI-generated)')
55
91
  .argument('[url]', 'Target public URL (http/https)')
@@ -94,6 +130,14 @@ export const create = new Command('create')
94
130
  data = await call();
95
131
  }
96
132
  else {
133
+ // #122 — Pinch shows up front, working, while the server-side wizard
134
+ // runs (legitimately 30-250s+, see LONG_RUN_TIMEOUT_MS above). Belt-
135
+ // and-suspenders `!opts.json` alongside pinchEnabled(): we're already
136
+ // inside the non-`--json` branch, but the check is kept explicit here
137
+ // too so stdout purity under `--json` (#106-F2/#121) never depends on
138
+ // this code staying inside that branch.
139
+ if (!opts.json && pinchEnabled())
140
+ console.log(renderPinch('thinking'));
97
141
  try {
98
142
  data = await spin(call, {
99
143
  text: `Creating a scrap from ${url}…`,
@@ -137,8 +181,15 @@ export const create = new Command('create')
137
181
  console.log(chalk.dim(` First run: `) + firstRunLabel(data, autoFixEnabled));
138
182
  if (schedule)
139
183
  console.log(chalk.dim(` Schedule: `) + schedule);
140
- if (scrapId)
141
- console.log(chalk.dim(` Next step: `) + `trawl get ${scrapId}`);
184
+ // #121 — on a successful first run, prove the value: show a small data
185
+ // sample (best-effort, silent on failure) before the next-step hint.
186
+ if (data.success && data.historyId)
187
+ await printDataSample(data.historyId);
188
+ // #121 — the natural next command is the DATA, not the metadata: point
189
+ // at `trawl data` primarily, keep `trawl get` as the secondary detail view.
190
+ if (scrapId) {
191
+ console.log(chalk.dim(` Next step: `) + `trawl data ${scrapId}` + chalk.dim(` (details: trawl get ${scrapId})`));
192
+ }
142
193
  // #114-F3 — only claim a background retry is happening when a scrap
143
194
  // actually exists to retry (never fabricate progress that isn't real);
144
195
  // `firstRunLabel` above already covers the !scrap / autofix-disabled
@@ -150,6 +201,13 @@ export const create = new Command('create')
150
201
  console.log(chalk.yellow(` Note: `) +
151
202
  `Auto-fix is retrying in the background — do NOT re-run create; poll ${pollTarget}.`);
152
203
  }
204
+ // #122 — Pinch reacts to the HONEST first-run outcome (same signal the
205
+ // ✓/✗ line above already renders): celebrates a real success, looks
206
+ // confused on a genuine failure. Belt-and-suspenders `!opts.json`
207
+ // alongside pinchEnabled() — see the 'thinking' print above for why.
208
+ if (!opts.json && pinchEnabled()) {
209
+ console.log(data.success ? renderPinch('celebrating') : renderPinch('confused'));
210
+ }
153
211
  }
154
212
  // Honest exit code alongside the honest payload — a --json caller gets
155
213
  // the raw body regardless (never wrapped/altered), but a script checking
@@ -3,15 +3,21 @@
3
3
  * art. Distinct from Clawd's 8-bit lane: Pinch is drawn with full 24-bit
4
4
  * (`\x1b[38;2;r;g;bm` / `\x1b[48;2;r;g;bm`) color blocks, not a fixed palette.
5
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.
6
+ * The 14×9 cube-grid is packed two grid rows into one terminal row: the
7
+ * upper row's color becomes the half-block's foreground, the lower row's
8
+ * becomes its background, using the upper-half-block glyph '▀' (or '▄' when
9
+ * only the lower half is filled). A 9-row grid therefore renders in 5
10
+ * terminal rows, 14 columns wide. '.' cells are transparent — no color
11
+ * escape is emitted for that half, so the terminal's own background shows
12
+ * through.
13
13
  *
14
- * See comes-io/trawl_cli#94.
14
+ * Grids + palette are mirrored from trawl_vue
15
+ * `src/modules/trawl/assets/mascot/_src/pinch.model.mjs` (AVCOLORS +
16
+ * AVATAR_GRIDS, rev r6f) — no cross-repo import (cli is a standalone npm
17
+ * package). Claws are 2×2 'O' blocks at cols 0-1 / 12-13, fully outside the
18
+ * body silhouette (on the sides), per r6f.
19
+ *
20
+ * See comes-io/trawl_cli#94, comes-io/trawl_cli#122.
15
21
  */
16
22
  export type PinchState = 'wave' | 'thinking' | 'celebrating' | 'confused';
17
23
  /**
@@ -19,8 +25,8 @@ export type PinchState = 'wave' | 'thinking' | 'celebrating' | 'confused';
19
25
  * one-line caption. Pure — never touches process.env/stdout; callers must
20
26
  * gate on `pinchEnabled()` before printing the result.
21
27
  *
22
- * `frame` only affects 'thinking' (2-frame spinner alternation); every other
23
- * state ignores it.
28
+ * `frame` is accepted for signature stability but currently unused — every
29
+ * state's grid is static (see `gridForState`).
24
30
  */
25
31
  export declare function renderPinch(state: PinchState, frame?: number): string;
26
32
  /**
package/dist/lib/pinch.js CHANGED
@@ -3,76 +3,98 @@
3
3
  * art. Distinct from Clawd's 8-bit lane: Pinch is drawn with full 24-bit
4
4
  * (`\x1b[38;2;r;g;bm` / `\x1b[48;2;r;g;bm`) color blocks, not a fixed palette.
5
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.
6
+ * The 14×9 cube-grid is packed two grid rows into one terminal row: the
7
+ * upper row's color becomes the half-block's foreground, the lower row's
8
+ * becomes its background, using the upper-half-block glyph '▀' (or '▄' when
9
+ * only the lower half is filled). A 9-row grid therefore renders in 5
10
+ * terminal rows, 14 columns wide. '.' cells are transparent — no color
11
+ * escape is emitted for that half, so the terminal's own background shows
12
+ * through.
13
13
  *
14
- * See comes-io/trawl_cli#94.
14
+ * Grids + palette are mirrored from trawl_vue
15
+ * `src/modules/trawl/assets/mascot/_src/pinch.model.mjs` (AVCOLORS +
16
+ * AVATAR_GRIDS, rev r6f) — no cross-repo import (cli is a standalone npm
17
+ * package). Claws are 2×2 'O' blocks at cols 0-1 / 12-13, fully outside the
18
+ * body silhouette (on the sides), per r6f.
19
+ *
20
+ * See comes-io/trawl_cli#94, comes-io/trawl_cli#122.
15
21
  */
16
- /** Grid-char → RGB. 'C' (cyan) is a "thinking" blip, not present in BASE_GRID. */
22
+ /** Grid-char → RGB, ported from AVCOLORS (pinch.model.mjs, rev r6f). */
17
23
  const PALETTE = {
18
- B: [0x3b, 0x82, 0xf6], // blue — shell
19
- O: [0xee, 0x90, 0x18], // orange — antennae / claws
24
+ B: [0x29, 0x79, 0xff], // blue — shell
25
+ O: [0xfb, 0x92, 0x3c], // orange — antennae / claws
20
26
  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
27
+ K: [0x12, 0x30, 0x33], // navy — pupils / mouth / blush-adjacent mouth corner
28
+ P: [0xf4, 0x72, 0xb6], // pink — blush
24
29
  };
25
30
  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
- ];
31
+ /**
32
+ * Explicit per-state 14×9 grids, ported verbatim from trawl_vue's
33
+ * AVATAR_GRIDS (pinch.model.mjs, rev r6f) — no derivation/mutation from a
34
+ * shared base, so each state stays a straight, auditable copy of its source
35
+ * grid. CLI state → source grid: wave→wave, thinking→working (a.k.a.
36
+ * AVGRID/idle), celebrating→celebrating, confused→confused.
37
+ */
38
+ const GRIDS = {
39
+ wave: [
40
+ '....O....O....',
41
+ '....O....O....',
42
+ '..BBBBBBBBBB..',
43
+ '..BWWBBBBWWBOO',
44
+ '..BWKBBBBKWBOO',
45
+ '..BBBBBBBBBB..',
46
+ 'OOBBPKBBKPBB..',
47
+ 'OOBBBBKKBBBB..',
48
+ '..............',
49
+ ],
50
+ // 'thinking' maps to the source's 'working' grid (identical to 'idle').
51
+ // The frame param is accepted for signature compatibility but ignored — a
52
+ // static working frame rather than an animated blip (#122, simplified;
53
+ // the source has no per-frame "thinking" animation to port).
54
+ thinking: [
55
+ '....O....O....',
56
+ '....O....O....',
57
+ '..BBBBBBBBBB..',
58
+ '..BWWBBBBWWB..',
59
+ '..BWKBBBBKWB..',
60
+ '..BBBBBBBBBB..',
61
+ 'OOBBPKBBKPBBOO',
62
+ 'OOBBBBKKBBBBOO',
63
+ '..............',
64
+ ],
65
+ celebrating: [
66
+ '....O....O....',
67
+ '....O....O....',
68
+ '..BBBBBBBBBB..',
69
+ 'OOBWWBBBBWWBOO',
70
+ 'OOBWKBBBBKWBOO',
71
+ '..BBBBBBBBBB..',
72
+ '..BBPKKKKPBB..',
73
+ '..BBBKKKKBBB..',
74
+ '..............',
75
+ ],
76
+ confused: [
77
+ '..............',
78
+ '...O......O...',
79
+ '..BBBBBBBBBB..',
80
+ '..BWWBBBBWWB..',
81
+ '..BWKBBBBKWB..',
82
+ '..BBBBBBBBBB..',
83
+ 'OOBBPBBBBPBBOO',
84
+ 'OOBBBBKKBBBBOO',
85
+ '..............',
86
+ ],
87
+ };
38
88
  const CAPTIONS = {
39
89
  wave: 'Pinch says hi.',
40
90
  thinking: 'Pinch is thinking…',
41
91
  celebrating: 'Pinch is celebrating!',
42
92
  confused: 'Pinch looks confused.',
43
93
  };
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
- }
94
+ /** Look up the grid for `state`. `frame` is accepted (signature stability
95
+ * for `renderPinch`) but unused — every current state's grid is static. */
96
+ function gridForState(state, _frame) {
97
+ return GRIDS[state];
76
98
  }
77
99
  const RESET = '\x1b[0m';
78
100
  const fgCode = ([r, g, b]) => `\x1b[38;2;${r};${g};${b}m`;
@@ -117,15 +139,17 @@ function renderGrid(rows) {
117
139
  * one-line caption. Pure — never touches process.env/stdout; callers must
118
140
  * gate on `pinchEnabled()` before printing the result.
119
141
  *
120
- * `frame` only affects 'thinking' (2-frame spinner alternation); every other
121
- * state ignores it.
142
+ * `frame` is accepted for signature stability but currently unused — every
143
+ * state's grid is static (see `gridForState`).
122
144
  */
123
145
  export function renderPinch(state, frame = 0) {
124
146
  const grid = gridForState(state, frame);
125
147
  const lines = renderGrid(grid);
126
148
  if (state === 'confused') {
127
149
  // "beside the art" — a bold '?' to the right of the eye row (grid rows
128
- // 4-5 pack into terminal line index 2).
150
+ // 4-5 pack into terminal line index 2, unchanged by the r6f grid — the
151
+ // antenna/shell/eye/claw row layout stayed the same, only width + claw
152
+ // placement changed).
129
153
  lines[2] = `${lines[2]} \x1b[1m?${RESET}`;
130
154
  }
131
155
  return [...lines, CAPTIONS[state]].join('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trawlme/cli",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Trawl CLI — manage scraps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {