@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 +46 -4
- package/dist/commands/scraps.js +6 -0
- package/dist/commands/telemetry.d.ts +2 -0
- package/dist/commands/telemetry.js +43 -0
- package/dist/index.js +24 -0
- package/dist/lib/api.js +10 -1
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.js +2 -0
- package/dist/lib/posthog.d.ts +5 -0
- package/dist/lib/posthog.js +52 -0
- package/package.json +2 -1
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
|
|
77
|
-
|
|
78
|
-
| `TRAWL_TOKEN`
|
|
79
|
-
| `TRAWL_API_URL`
|
|
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
|
|
package/dist/commands/scraps.js
CHANGED
|
@@ -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,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
|
});
|
package/dist/lib/config.d.ts
CHANGED
package/dist/lib/config.js
CHANGED
|
@@ -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.
|
|
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": {
|