content-grade 1.0.38 → 1.0.41
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/bin/content-grade.js +15 -11
- package/bin/telemetry.js +58 -3
- package/dist-server/server/routes/analytics.js +23 -0
- package/package.json +1 -1
package/bin/content-grade.js
CHANGED
|
@@ -21,7 +21,7 @@ import { fileURLToPath } from 'url';
|
|
|
21
21
|
import { promisify } from 'util';
|
|
22
22
|
import { get as httpsGet } from 'https';
|
|
23
23
|
import { get as httpGet } from 'http';
|
|
24
|
-
import { initTelemetry, recordEvent, disableTelemetry, enableTelemetry, telemetryStatus, isOptedOut } from './telemetry.js';
|
|
24
|
+
import { initTelemetry, recordEvent, ping, disableTelemetry, enableTelemetry, telemetryStatus, isOptedOut } from './telemetry.js';
|
|
25
25
|
|
|
26
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
27
|
const root = resolve(__dirname, '..');
|
|
@@ -160,7 +160,7 @@ function showFreeTierCTA(count) {
|
|
|
160
160
|
} else {
|
|
161
161
|
// Runs available — always visible count + clear Pro benefits + Stripe link
|
|
162
162
|
console.log(` ${WH}${count}/${limit} runs used · ${remaining} remaining${R}`);
|
|
163
|
-
console.log(` ${
|
|
163
|
+
console.log(` ${WH} Pro: unlimited runs · batch analysis · priority support${R}`);
|
|
164
164
|
console.log(` ${MG}→ Upgrade ($9/mo): ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
165
165
|
}
|
|
166
166
|
hr();
|
|
@@ -276,20 +276,20 @@ function checkFreeTierLimit() {
|
|
|
276
276
|
|
|
277
277
|
blank();
|
|
278
278
|
hr();
|
|
279
|
-
console.log(` ${WH}${B}You've used all ${limit} free
|
|
279
|
+
console.log(` ${WH}${B}You've used all ${limit} free analyses.${R}`);
|
|
280
280
|
blank();
|
|
281
|
-
console.log(` ${WH}Pro
|
|
282
|
-
console.log(` ${
|
|
283
|
-
console.log(` ${
|
|
284
|
-
console.log(` ${
|
|
285
|
-
console.log(` ${
|
|
281
|
+
console.log(` ${WH}${B}Pro removes the limit — unlimited runs, forever.${R}`);
|
|
282
|
+
console.log(` ${WH} · Unlimited runs — no cap, no counter${R}`);
|
|
283
|
+
console.log(` ${WH} · Batch analysis — grade entire /posts/ directories at once${R}`);
|
|
284
|
+
console.log(` ${WH} · Priority support — direct response within 24h${R}`);
|
|
285
|
+
console.log(` ${WH} · CI integration — exit codes, JSON/HTML output${R}`);
|
|
286
286
|
blank();
|
|
287
287
|
console.log(` ${MG}${B}→ Get Pro ($9/mo): ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
288
288
|
blank();
|
|
289
|
-
console.log(` ${D}After
|
|
289
|
+
console.log(` ${D}After purchase — activate in 2 steps:${R}`);
|
|
290
290
|
console.log(` ${D} 1. Get your key: ${CY}https://content-grade.onrender.com/my-license${R}`);
|
|
291
|
-
console.log(` ${D} 2.
|
|
292
|
-
console.log(` ${D}
|
|
291
|
+
console.log(` ${D} 2. Run: ${CY}content-grade activate <your-key>${R}`);
|
|
292
|
+
console.log(` ${D} Already have a key? Run step 2 now.${R}`);
|
|
293
293
|
hr();
|
|
294
294
|
blank();
|
|
295
295
|
return true;
|
|
@@ -2163,6 +2163,10 @@ if (_telem.isNew) {
|
|
|
2163
2163
|
recordEvent({ event: 'install' });
|
|
2164
2164
|
}
|
|
2165
2165
|
|
|
2166
|
+
// Fire-and-forget ping on every run — 500ms timeout, never blocks.
|
|
2167
|
+
// Sends { command, version, timestamp, anonymous_id } to /ping.
|
|
2168
|
+
ping(cmd || 'none');
|
|
2169
|
+
|
|
2166
2170
|
// ── License key startup validation ───────────────────────────────────────────
|
|
2167
2171
|
// Background check: re-validate stored license key against server every 7 days.
|
|
2168
2172
|
// Non-blocking — never delays the CLI. Clears invalid/revoked keys silently.
|
package/bin/telemetry.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
|
|
12
|
-
import { homedir } from 'os';
|
|
12
|
+
import { homedir, hostname } from 'os';
|
|
13
13
|
import { join, resolve, dirname } from 'path';
|
|
14
14
|
import { createHash } from 'crypto';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
@@ -27,9 +27,11 @@ const EVENTS_FILE = join(CONFIG_DIR, 'events.jsonl');
|
|
|
27
27
|
|
|
28
28
|
// Telemetry endpoint: CLI events are forwarded here when telemetry is enabled.
|
|
29
29
|
// Set CONTENT_GRADE_TELEMETRY_URL to enable remote aggregation (e.g. self-hosted endpoint).
|
|
30
|
-
// Default: local-only (events stored at ~/.content-grade/events.jsonl, no remote send).
|
|
31
30
|
const REMOTE_URL = process.env.CONTENT_GRADE_TELEMETRY_URL || 'https://content-grade.onrender.com/api/telemetry';
|
|
32
31
|
|
|
32
|
+
// Lightweight ping endpoint — one POST per CLI run, 500ms hard timeout.
|
|
33
|
+
const DEFAULT_PING_URL = 'https://content-grade.onrender.com/api/ping';
|
|
34
|
+
|
|
33
35
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
34
36
|
|
|
35
37
|
function ensureDir() {
|
|
@@ -53,6 +55,21 @@ function generateId() {
|
|
|
53
55
|
.slice(0, 16);
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Returns a stable, anonymous machine ID — no PII.
|
|
60
|
+
* Tries /etc/machine-id (Linux), falls back to hashing hostname.
|
|
61
|
+
* Same machine always produces the same ID. Survives reinstalls.
|
|
62
|
+
*/
|
|
63
|
+
function getAnonymousId() {
|
|
64
|
+
let source = '';
|
|
65
|
+
try { source = readFileSync('/etc/machine-id', 'utf8').trim(); } catch {}
|
|
66
|
+
if (!source) {
|
|
67
|
+
try { source = readFileSync('/var/lib/dbus/machine-id', 'utf8').trim(); } catch {}
|
|
68
|
+
}
|
|
69
|
+
if (!source) source = hostname();
|
|
70
|
+
return createHash('sha256').update(source).digest('hex').slice(0, 32);
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
57
74
|
|
|
58
75
|
/**
|
|
@@ -232,6 +249,44 @@ export function telemetryStatus() {
|
|
|
232
249
|
};
|
|
233
250
|
}
|
|
234
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Fire-and-forget ping on each CLI run.
|
|
254
|
+
* Sends { command, version, timestamp, anonymous_id } to /ping.
|
|
255
|
+
* 500ms hard timeout — never blocks the CLI.
|
|
256
|
+
* URL reads from config (telemetryPingUrl), falls back to DEFAULT_PING_URL.
|
|
257
|
+
*/
|
|
258
|
+
export async function ping(command) {
|
|
259
|
+
if (isOptedOut()) return;
|
|
260
|
+
const cfg = loadConfig();
|
|
261
|
+
const url = cfg?.telemetryPingUrl || DEFAULT_PING_URL;
|
|
262
|
+
|
|
263
|
+
// Persist URL in config if not already set (one-time write)
|
|
264
|
+
if (!cfg?.telemetryPingUrl) {
|
|
265
|
+
saveConfig({ ...(cfg || {}), telemetryPingUrl: DEFAULT_PING_URL });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
const timer = setTimeout(() => controller.abort(), 500);
|
|
270
|
+
try {
|
|
271
|
+
await fetch(url, {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: { 'Content-Type': 'application/json' },
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
command,
|
|
276
|
+
version: _pkgVersion,
|
|
277
|
+
timestamp: new Date().toISOString(),
|
|
278
|
+
anonymous_id: getAnonymousId(),
|
|
279
|
+
platform: process.platform,
|
|
280
|
+
}),
|
|
281
|
+
signal: controller.signal,
|
|
282
|
+
});
|
|
283
|
+
} catch {
|
|
284
|
+
// Network failure — silently ignore, never interrupt the CLI
|
|
285
|
+
} finally {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
235
290
|
// ── Rate limiting ─────────────────────────────────────────────────────────────
|
|
236
291
|
|
|
237
292
|
const FREE_LIFETIME_LIMIT = 15;
|
|
@@ -290,7 +345,7 @@ function _write(event) {
|
|
|
290
345
|
|
|
291
346
|
async function _sendRemote(event) {
|
|
292
347
|
const controller = new AbortController();
|
|
293
|
-
const timer = setTimeout(() => controller.abort(),
|
|
348
|
+
const timer = setTimeout(() => controller.abort(), 500);
|
|
294
349
|
try {
|
|
295
350
|
await fetch(REMOTE_URL, {
|
|
296
351
|
method: 'POST',
|
|
@@ -31,6 +31,29 @@ function thirtyDaysAgo() {
|
|
|
31
31
|
return d.toISOString().slice(0, 10);
|
|
32
32
|
}
|
|
33
33
|
export function registerAnalyticsRoutes(app) {
|
|
34
|
+
// ── Lightweight ping — fire-and-forget per CLI run ────────────────────────
|
|
35
|
+
// Receives { command, version, timestamp, anonymous_id } from CLI.
|
|
36
|
+
// No PII. Maps anonymous_id → install_id for unique user counts.
|
|
37
|
+
// Always returns 200 — never interrupt a CLI session.
|
|
38
|
+
app.post('/api/ping', async (req) => {
|
|
39
|
+
try {
|
|
40
|
+
const body = req.body;
|
|
41
|
+
const anonymousId = body?.anonymous_id ? String(body.anonymous_id).slice(0, 64) : null;
|
|
42
|
+
if (!anonymousId)
|
|
43
|
+
return { ok: true };
|
|
44
|
+
const db = getDb();
|
|
45
|
+
const b = body;
|
|
46
|
+
db.prepare(`
|
|
47
|
+
INSERT INTO cli_telemetry
|
|
48
|
+
(install_id, event, command, version, platform, created_at)
|
|
49
|
+
VALUES (?, 'ping', ?, ?, ?, CURRENT_TIMESTAMP)
|
|
50
|
+
`).run(anonymousId, b.command ? String(b.command).slice(0, 64) : null, b.version ? String(b.version).slice(0, 32) : null, b.platform ? String(b.platform).slice(0, 32) : null);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// never fail — telemetry is non-critical
|
|
54
|
+
}
|
|
55
|
+
return { ok: true };
|
|
56
|
+
});
|
|
34
57
|
// ── CLI telemetry receiver ────────────────────────────────────────────────
|
|
35
58
|
// Receives events from CLI users who have opted in to telemetry.
|
|
36
59
|
// Always returns 200 — never interrupt a CLI session for analytics.
|
package/package.json
CHANGED