@trawlme/cli 1.11.0 → 1.13.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 +16 -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
|
@@ -91,6 +91,7 @@ scraps
|
|
|
91
91
|
console.log(chalk.dim(` Last run: `) + lastRun(data));
|
|
92
92
|
console.log(chalk.dim(` Updated: `) + new Date(data.updatedAt).toLocaleString());
|
|
93
93
|
});
|
|
94
|
+
const VALID_TIERS = ['tier0', 'tier1', 'tier2', 'tier3', 'tier4'];
|
|
94
95
|
// create
|
|
95
96
|
scraps
|
|
96
97
|
.command('create')
|
|
@@ -99,13 +100,20 @@ scraps
|
|
|
99
100
|
.option('-u, --url <url>', 'Target URL')
|
|
100
101
|
.option('-r, --request <request>', 'Request/query')
|
|
101
102
|
.option('-d, --description <text>', 'Scrap description')
|
|
103
|
+
.option('--tier <tier>', `Force proxy tier (${VALID_TIERS.join('|')})`)
|
|
102
104
|
.action(async (opts) => {
|
|
105
|
+
if (opts.tier !== undefined && !VALID_TIERS.includes(opts.tier)) {
|
|
106
|
+
console.log(chalk.red(`✗ Invalid --tier "${opts.tier}" (allowed: ${VALID_TIERS.join(', ')})`));
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
103
110
|
const spinner = ora('Creating scrap…').start();
|
|
104
111
|
const data = await api.post('/api/scraps', {
|
|
105
112
|
title: opts.title,
|
|
106
113
|
...(opts.url && { url: opts.url }),
|
|
107
114
|
request: opts.request || '',
|
|
108
115
|
...(opts.description !== undefined && { description: opts.description }),
|
|
116
|
+
...(opts.tier !== undefined && { proxyTier: opts.tier }),
|
|
109
117
|
});
|
|
110
118
|
spinner.succeed(`Scrap created: ${chalk.bold(data._id)}`);
|
|
111
119
|
console.log(chalk.dim(` Title: ${data.title}`));
|
|
@@ -126,8 +134,14 @@ scraps
|
|
|
126
134
|
.option('--no-autofix', 'Disable AI Fix')
|
|
127
135
|
.option('-p, --params <json>', 'Runtime params as JSON array of objects (e.g. \'[{"TRAWL.paramName":"value"}]\')')
|
|
128
136
|
.option('--params-file <path>', 'Runtime params from a JSON file')
|
|
137
|
+
.option('--tier <tier>', `Force proxy tier (${VALID_TIERS.join('|')})`)
|
|
129
138
|
.action(async (id, opts) => {
|
|
130
139
|
validateObjectId(id);
|
|
140
|
+
if (opts.tier !== undefined && !VALID_TIERS.includes(opts.tier)) {
|
|
141
|
+
console.log(chalk.red(`✗ Invalid --tier "${opts.tier}" (allowed: ${VALID_TIERS.join(', ')})`));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
131
145
|
const body = {};
|
|
132
146
|
if (opts.title !== undefined)
|
|
133
147
|
body.title = opts.title;
|
|
@@ -174,6 +188,8 @@ scraps
|
|
|
174
188
|
}
|
|
175
189
|
body.params = parsed;
|
|
176
190
|
}
|
|
191
|
+
if (opts.tier !== undefined)
|
|
192
|
+
body.proxyTier = opts.tier;
|
|
177
193
|
if (Object.keys(body).length === 0) {
|
|
178
194
|
console.log(chalk.yellow('Nothing to update. Provide at least one option.'));
|
|
179
195
|
return;
|
|
@@ -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.13.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": {
|