content-grade 1.0.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/CONTRIBUTING.md +198 -0
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/bin/content-grade.js +1033 -0
- package/bin/telemetry.js +230 -0
- package/dist/assets/index-BUN69TiT.js +78 -0
- package/dist/index.html +22 -0
- package/dist-server/server/app.js +36 -0
- package/dist-server/server/claude.js +22 -0
- package/dist-server/server/db.js +79 -0
- package/dist-server/server/index.js +62 -0
- package/dist-server/server/routes/demos.js +1701 -0
- package/dist-server/server/routes/stripe.js +136 -0
- package/dist-server/server/services/claude.js +55 -0
- package/dist-server/server/services/stripe.js +109 -0
- package/package.json +84 -0
package/bin/telemetry.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContentGrade Telemetry
|
|
3
|
+
*
|
|
4
|
+
* Privacy-first, anonymous usage tracking.
|
|
5
|
+
* - No file contents, no PII, no account info
|
|
6
|
+
* - Stored locally at ~/.content-grade/events.jsonl
|
|
7
|
+
* - Opt out: --no-telemetry flag, CONTENT_GRADE_NO_TELEMETRY=1, or run: content-grade telemetry off
|
|
8
|
+
* - Remote endpoint: set CONTENT_GRADE_TELEMETRY_URL to forward events
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { createHash } from 'crypto';
|
|
15
|
+
|
|
16
|
+
const CONFIG_DIR = join(homedir(), '.content-grade');
|
|
17
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
18
|
+
const EVENTS_FILE = join(CONFIG_DIR, 'events.jsonl');
|
|
19
|
+
|
|
20
|
+
// Telemetry endpoint: set CONTENT_GRADE_TELEMETRY_URL to enable remote reporting
|
|
21
|
+
const REMOTE_URL = process.env.CONTENT_GRADE_TELEMETRY_URL || null;
|
|
22
|
+
|
|
23
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function ensureDir() {
|
|
26
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadConfig() {
|
|
30
|
+
try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); }
|
|
31
|
+
catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveConfig(cfg) {
|
|
35
|
+
ensureDir();
|
|
36
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function generateId() {
|
|
40
|
+
return createHash('sha256')
|
|
41
|
+
.update(`${Date.now()}-${Math.random()}-${process.pid}-${homedir()}`)
|
|
42
|
+
.digest('hex')
|
|
43
|
+
.slice(0, 16);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if telemetry is disabled.
|
|
50
|
+
* Order of precedence: CLI flag > env var > config file.
|
|
51
|
+
*/
|
|
52
|
+
export function isOptedOut() {
|
|
53
|
+
if (process.argv.includes('--no-telemetry')) return true;
|
|
54
|
+
if (process.env.CONTENT_GRADE_NO_TELEMETRY === '1') return true;
|
|
55
|
+
const cfg = loadConfig();
|
|
56
|
+
return cfg?.telemetryEnabled === false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initialise telemetry on CLI startup.
|
|
61
|
+
* Returns { installId, isNew, optedOut }.
|
|
62
|
+
* isNew=true means this is the first time the CLI has run — show the notice.
|
|
63
|
+
*/
|
|
64
|
+
export function initTelemetry() {
|
|
65
|
+
if (isOptedOut()) return { installId: null, isNew: false, optedOut: true };
|
|
66
|
+
|
|
67
|
+
const cfg = loadConfig();
|
|
68
|
+
|
|
69
|
+
if (cfg?.installId) {
|
|
70
|
+
return { installId: cfg.installId, isNew: false, optedOut: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// First run — generate anonymous install ID
|
|
74
|
+
const installId = generateId();
|
|
75
|
+
saveConfig({
|
|
76
|
+
installId,
|
|
77
|
+
installedAt: new Date().toISOString(),
|
|
78
|
+
telemetryEnabled: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Record install event
|
|
82
|
+
_write({ event: 'install', installId, platform: process.platform, nodeVersion: process.version });
|
|
83
|
+
|
|
84
|
+
return { installId, isNew: true, optedOut: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Record a usage event.
|
|
89
|
+
* Safe to call from anywhere — never throws, never blocks CLI.
|
|
90
|
+
*
|
|
91
|
+
* Tracked fields (safe, no PII):
|
|
92
|
+
* event — 'command'
|
|
93
|
+
* command — 'analyze' | 'headline' | 'demo' | 'start' | 'init' | 'help'
|
|
94
|
+
* duration_ms — wall time
|
|
95
|
+
* success — boolean
|
|
96
|
+
* exit_code — process exit code (set by trackCommand)
|
|
97
|
+
* score — numeric score if available (analyze/headline result)
|
|
98
|
+
* content_type — content category if available (analyze result)
|
|
99
|
+
* version — CLI version
|
|
100
|
+
*/
|
|
101
|
+
export function recordEvent(data) {
|
|
102
|
+
if (isOptedOut()) return;
|
|
103
|
+
|
|
104
|
+
const cfg = loadConfig();
|
|
105
|
+
const installId = cfg?.installId;
|
|
106
|
+
if (!installId) return; // shouldn't happen after initTelemetry, but guard
|
|
107
|
+
|
|
108
|
+
const event = {
|
|
109
|
+
...data,
|
|
110
|
+
installId,
|
|
111
|
+
timestamp: new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
_write(event);
|
|
115
|
+
|
|
116
|
+
if (REMOTE_URL) {
|
|
117
|
+
_sendRemote(event); // fire-and-forget
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Wrap a command handler to automatically record timing, success, and result metadata.
|
|
123
|
+
*
|
|
124
|
+
* The wrapped fn can return an optional { score, content_type } for richer tracking.
|
|
125
|
+
*/
|
|
126
|
+
export function trackCommand(command, fn) {
|
|
127
|
+
return async (...args) => {
|
|
128
|
+
const start = Date.now();
|
|
129
|
+
let success = false;
|
|
130
|
+
let meta = {};
|
|
131
|
+
|
|
132
|
+
// Register exit handler so we capture exit(1) paths too
|
|
133
|
+
process.once('exit', (code) => {
|
|
134
|
+
if (!success) {
|
|
135
|
+
recordEvent({
|
|
136
|
+
event: 'command',
|
|
137
|
+
command,
|
|
138
|
+
duration_ms: Date.now() - start,
|
|
139
|
+
success: code === 0,
|
|
140
|
+
exit_code: code,
|
|
141
|
+
...meta,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const result = await fn(...args);
|
|
148
|
+
success = true;
|
|
149
|
+
if (result?.score != null) meta.score = result.score;
|
|
150
|
+
if (result?.content_type) meta.content_type = result.content_type;
|
|
151
|
+
|
|
152
|
+
recordEvent({
|
|
153
|
+
event: 'command',
|
|
154
|
+
command,
|
|
155
|
+
duration_ms: Date.now() - start,
|
|
156
|
+
success: true,
|
|
157
|
+
exit_code: 0,
|
|
158
|
+
...meta,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
// re-throw — the exit handler will record failure
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Disable telemetry and persist to config.
|
|
171
|
+
* Called by: content-grade telemetry off
|
|
172
|
+
*/
|
|
173
|
+
export function disableTelemetry() {
|
|
174
|
+
const cfg = loadConfig() || {};
|
|
175
|
+
cfg.telemetryEnabled = false;
|
|
176
|
+
saveConfig(cfg);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Enable telemetry (undo opt-out).
|
|
181
|
+
* Called by: content-grade telemetry on
|
|
182
|
+
*/
|
|
183
|
+
export function enableTelemetry() {
|
|
184
|
+
const cfg = loadConfig() || {};
|
|
185
|
+
cfg.telemetryEnabled = true;
|
|
186
|
+
if (!cfg.installId) cfg.installId = generateId();
|
|
187
|
+
saveConfig(cfg);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Show telemetry status.
|
|
192
|
+
*/
|
|
193
|
+
export function telemetryStatus() {
|
|
194
|
+
const optedOut = isOptedOut();
|
|
195
|
+
const cfg = loadConfig();
|
|
196
|
+
return {
|
|
197
|
+
enabled: !optedOut,
|
|
198
|
+
installId: cfg?.installId || null,
|
|
199
|
+
eventsFile: EVENTS_FILE,
|
|
200
|
+
remoteUrl: REMOTE_URL || '(none — local only)',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Internal ──────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
function _write(event) {
|
|
207
|
+
try {
|
|
208
|
+
ensureDir();
|
|
209
|
+
appendFileSync(EVENTS_FILE, JSON.stringify(event) + '\n');
|
|
210
|
+
} catch {
|
|
211
|
+
// Never fail silently vs crashing the CLI
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function _sendRemote(event) {
|
|
216
|
+
const controller = new AbortController();
|
|
217
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
218
|
+
try {
|
|
219
|
+
await fetch(REMOTE_URL, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
body: JSON.stringify(event),
|
|
223
|
+
signal: controller.signal,
|
|
224
|
+
});
|
|
225
|
+
} catch {
|
|
226
|
+
// Network failure — silently ignore, never interrupt the CLI
|
|
227
|
+
} finally {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
}
|
|
230
|
+
}
|