@trawlme/cli 1.10.0 → 1.12.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
@@ -71,12 +71,54 @@ Skills auto-update silently when you upgrade the CLI — no need to re-install m
71
71
 
72
72
  You can also install skills standalone (without the CLI): `npx @trawlme/skills install`.
73
73
 
74
+ ### Telemetry
75
+
76
+ ```
77
+ trawl telemetry on Enable usage telemetry (default)
78
+ trawl telemetry off Disable usage telemetry
79
+ trawl telemetry status Show current state, telemetry ID, and opt-out instructions
80
+ ```
81
+
82
+ ## Telemetry
83
+
84
+ `@trawlme/cli` collects anonymous usage data to help us improve the CLI based on real usage patterns. Telemetry is enabled by default.
85
+
86
+ **What we collect:**
87
+ - Command name (e.g. `scraps list`)
88
+ - CLI version
89
+ - Node.js version
90
+ - Platform (e.g. `darwin`, `linux`, `win32`)
91
+ - Command duration (`duration_ms`)
92
+ - Exit code (`0` = success, `1` = error)
93
+ - Error name if the command failed (e.g. `ApiError`) — never the error message or stack
94
+ - An opaque, randomly-generated telemetry ID (`cli_<uuid>`) stored locally in your config file
95
+
96
+ **What we do NOT collect:**
97
+ - Email address or any account identifier
98
+ - Scrap content, scrap URLs, or request bodies
99
+ - Org names or any business data
100
+ - IP address (PostHog EU Cloud anonymises it)
101
+ - Any personally identifiable information (PII)
102
+
103
+ **How to opt out:**
104
+
105
+ ```bash
106
+ # Permanent opt-out (written to config file)
107
+ trawl telemetry off
108
+
109
+ # Session-level opt-out (env var, takes precedence)
110
+ TRAWL_TELEMETRY=0 trawl scraps list
111
+ ```
112
+
113
+ **Why:** usage data helps us prioritise CLI features and catch silent errors before users report them.
114
+
74
115
  ## Environment variables
75
116
 
76
- | Variable | Description |
77
- |---------------|------------------------------------------------------|
78
- | `TRAWL_TOKEN` | JWT token — bypasses login prompt, useful for CI/CD |
79
- | `TRAWL_API_URL` | Override the default API URL (`https://api.trawl.me`) |
117
+ | Variable | Description |
118
+ |--------------------|-------------------------------------------------------------------|
119
+ | `TRAWL_TOKEN` | JWT token — bypasses login prompt, useful for CI/CD |
120
+ | `TRAWL_API_URL` | Override the default API URL (`https://api.trawl.me`) |
121
+ | `TRAWL_TELEMETRY` | Set to `0` to disable telemetry for the current session |
80
122
 
81
123
  ## API documentation
82
124
 
@@ -122,6 +122,8 @@ scraps
122
122
  .option('--no-cron', 'Disable cron (set to null)')
123
123
  .option('--alert <email>', 'Failure alert email (empty string to clear)')
124
124
  .option('--no-alert', 'Disable failure alert email (set to null)')
125
+ .option('--autofix', 'Enable AI Fix (auto-recovery on selector breakage)')
126
+ .option('--no-autofix', 'Disable AI Fix')
125
127
  .option('-p, --params <json>', 'Runtime params as JSON array of objects (e.g. \'[{"TRAWL.paramName":"value"}]\')')
126
128
  .option('--params-file <path>', 'Runtime params from a JSON file')
127
129
  .action(async (id, opts) => {
@@ -143,6 +145,10 @@ scraps
143
145
  body.alert = null;
144
146
  else if (typeof opts.alert === 'string')
145
147
  body.alert = opts.alert === '' ? null : opts.alert;
148
+ if (opts.autofix === true)
149
+ body.autoFix = true;
150
+ else if (opts.autofix === false)
151
+ body.autoFix = false;
146
152
  if (opts.params !== undefined || opts.paramsFile !== undefined) {
147
153
  let raw;
148
154
  if (opts.paramsFile) {
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const telemetry: Command;
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import config from '../lib/config.js';
4
+ import { initPostHog } from '../lib/posthog.js';
5
+ export const telemetry = new Command('telemetry').description('Manage CLI telemetry (usage data collection)');
6
+ telemetry
7
+ .command('on')
8
+ .description('Enable telemetry (default)')
9
+ .action(() => {
10
+ config.set('telemetry', true);
11
+ // Ensure a telemetryUserId is created on opt-in
12
+ initPostHog();
13
+ console.log(chalk.green('✓ Telemetry enabled'));
14
+ console.log(chalk.dim(' Anonymous usage data will be collected. Run `trawl telemetry status` to view.'));
15
+ });
16
+ telemetry
17
+ .command('off')
18
+ .description('Disable telemetry')
19
+ .action(() => {
20
+ config.set('telemetry', false);
21
+ console.log(chalk.green('✓ Telemetry disabled'));
22
+ console.log(chalk.dim(' No data will be sent. Your telemetry ID is kept for if you re-enable later.'));
23
+ });
24
+ telemetry
25
+ .command('status')
26
+ .description('Show current telemetry state')
27
+ .action(() => {
28
+ const enabled = config.get('telemetry') !== false;
29
+ const userId = config.get('telemetryUserId') || '(not yet generated)';
30
+ const envOverride = process.env['TRAWL_TELEMETRY'] === '0';
31
+ console.log(chalk.bold('Telemetry status'));
32
+ console.log(chalk.dim(' State: ') +
33
+ (envOverride
34
+ ? chalk.yellow('disabled (TRAWL_TELEMETRY=0 env var)')
35
+ : enabled
36
+ ? chalk.green('enabled')
37
+ : chalk.yellow('disabled')));
38
+ console.log(chalk.dim(' Telemetry ID: ') + userId);
39
+ console.log('');
40
+ console.log(chalk.dim(' To opt out:'));
41
+ console.log(chalk.dim(' trawl telemetry off'));
42
+ console.log(chalk.dim(' TRAWL_TELEMETRY=0 (env var, disables for this session)'));
43
+ });
package/dist/index.js CHANGED
@@ -7,8 +7,11 @@ import { dirname, join } from 'node:path';
7
7
  import { login, logout } from './commands/login.js';
8
8
  import { scraps } from './commands/scraps.js';
9
9
  import { skills } from './commands/skills.js';
10
+ import { telemetry } from './commands/telemetry.js';
10
11
  import { autoUpdateInstalledSkills } from './lib/skills.js';
12
+ import { initPostHog, captureCommand, shutdown } from './lib/posthog.js';
11
13
  autoUpdateInstalledSkills();
14
+ initPostHog();
12
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
16
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
14
17
  const program = new Command()
@@ -16,11 +19,32 @@ const program = new Command()
16
19
  .description('Trawl CLI — manage scraps from the terminal')
17
20
  .version(pkg.version)
18
21
  .option('--debug', 'Show full error stack traces');
22
+ // Track start times per command instance for duration measurement
23
+ const startTimes = new WeakMap();
24
+ program.hook('preAction', (thisCommand, actionCommand) => {
25
+ startTimes.set(actionCommand, Date.now());
26
+ });
27
+ program.hook('postAction', (thisCommand, actionCommand) => {
28
+ const start = startTimes.get(actionCommand);
29
+ if (start !== undefined) {
30
+ const name = actionCommand.parent
31
+ ? `${actionCommand.parent.name()} ${actionCommand.name()}`
32
+ : actionCommand.name();
33
+ void captureCommand(name, { duration_ms: Date.now() - start, exit_code: 0 });
34
+ }
35
+ });
19
36
  program.addCommand(login);
20
37
  program.addCommand(logout);
21
38
  program.addCommand(scraps);
22
39
  program.addCommand(skills);
40
+ program.addCommand(telemetry);
41
+ process.on('exit', () => {
42
+ void shutdown();
43
+ });
23
44
  program.parseAsync().catch((err) => {
45
+ // Capture error telemetry (best-effort: command name from process.argv)
46
+ const name = process.argv.slice(2).filter((a) => !a.startsWith('-')).join(' ') || 'unknown';
47
+ void captureCommand(name, { exit_code: 1, error: err.name });
24
48
  const { debug } = program.opts();
25
49
  if (debug || process.env['DEBUG']) {
26
50
  console.error(err);
package/dist/lib/api.js CHANGED
@@ -1,4 +1,10 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, resolve } from 'node:path';
1
4
  import config from './config.js';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8'));
7
+ const USER_AGENT = `@trawlme/cli/${pkg.version}`;
2
8
  class ApiError extends Error {
3
9
  status;
4
10
  constructor(status, message) {
@@ -59,6 +65,7 @@ async function request(path, options = {}) {
59
65
  ...options,
60
66
  headers: {
61
67
  'Content-Type': 'application/json',
68
+ 'User-Agent': USER_AGENT,
62
69
  ...options.headers,
63
70
  Cookie: `TOKEN=${token}`,
64
71
  },
@@ -89,6 +96,7 @@ async function upload(path, formData) {
89
96
  method: 'POST',
90
97
  body: formData,
91
98
  headers: {
99
+ 'User-Agent': USER_AGENT,
92
100
  Cookie: `TOKEN=${token}`,
93
101
  },
94
102
  });
@@ -112,7 +120,7 @@ async function publicPost(path, body) {
112
120
  const url = `${config.get('apiUrl')}${path}`;
113
121
  const res = await fetch(url, {
114
122
  method: 'POST',
115
- headers: { 'Content-Type': 'application/json' },
123
+ headers: { 'Content-Type': 'application/json', 'User-Agent': USER_AGENT },
116
124
  body: body ? JSON.stringify(body) : undefined,
117
125
  });
118
126
  await throwIfError(res, true);
@@ -146,6 +154,7 @@ export const api = {
146
154
  const res = await fetch(url, {
147
155
  headers: {
148
156
  Accept: 'text/event-stream',
157
+ 'User-Agent': USER_AGENT,
149
158
  Cookie: `TOKEN=${token}`,
150
159
  },
151
160
  });
@@ -2,6 +2,8 @@ import Conf from 'conf';
2
2
  interface TrawlConfig {
3
3
  apiUrl: string;
4
4
  token: string;
5
+ telemetry: boolean;
6
+ telemetryUserId: string;
5
7
  }
6
8
  declare const config: Conf<TrawlConfig>;
7
9
  export default config;
@@ -4,6 +4,8 @@ const config = new Conf({
4
4
  defaults: {
5
5
  apiUrl: 'https://api.trawl.me',
6
6
  token: '',
7
+ telemetry: true,
8
+ telemetryUserId: '',
7
9
  },
8
10
  });
9
11
  export default config;
@@ -0,0 +1,5 @@
1
+ export declare const initPostHog: () => void;
2
+ export declare const captureCommand: (command: string, props?: Record<string, unknown>) => void;
3
+ export declare const shutdown: () => Promise<void>;
4
+ /** Reset singleton state — for testing only */
5
+ export declare const _resetForTesting: () => void;
@@ -0,0 +1,52 @@
1
+ import { PostHog } from 'posthog-node';
2
+ import os from 'node:os';
3
+ import crypto from 'node:crypto';
4
+ import { readFileSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { dirname, resolve } from 'node:path';
7
+ import config from './config.js';
8
+ // public-by-design — cf memory feedback_public_keys_in_config.md
9
+ const POSTHOG_KEY = 'phc_pGgSLivbq8rDZzXC9trWWJG7sBjNghXeTJjETzBcnEDv';
10
+ const POSTHOG_HOST = 'https://eu.i.posthog.com';
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8'));
13
+ let client = null;
14
+ let distinctId = null;
15
+ export const initPostHog = () => {
16
+ if (config.get('telemetry') === false || process.env['TRAWL_TELEMETRY'] === '0')
17
+ return;
18
+ if (!config.get('telemetryUserId')) {
19
+ config.set('telemetryUserId', `cli_${crypto.randomUUID()}`);
20
+ }
21
+ distinctId = config.get('telemetryUserId');
22
+ client = new PostHog(POSTHOG_KEY, {
23
+ host: POSTHOG_HOST,
24
+ flushAt: 1,
25
+ flushInterval: 0,
26
+ });
27
+ };
28
+ export const captureCommand = (command, props = {}) => {
29
+ if (!client || !distinctId)
30
+ return;
31
+ client.capture({
32
+ distinctId,
33
+ event: 'cli.command.run',
34
+ properties: {
35
+ command,
36
+ cli_version: pkg.version,
37
+ node_version: process.version,
38
+ platform: os.platform(),
39
+ source: 'cli',
40
+ ...props,
41
+ },
42
+ });
43
+ };
44
+ export const shutdown = async () => {
45
+ if (client)
46
+ await client.shutdown();
47
+ };
48
+ /** Reset singleton state — for testing only */
49
+ export const _resetForTesting = () => {
50
+ client = null;
51
+ distinctId = null;
52
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trawlme/cli",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Trawl CLI — manage scraps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,6 +45,7 @@
45
45
  "commander": "^14.0.3",
46
46
  "conf": "^15.1.0",
47
47
  "ora": "^9.3.0",
48
+ "posthog-node": "^4.0.0",
48
49
  "strip-ansi": "^7.2.0"
49
50
  },
50
51
  "devDependencies": {