content-grade 1.0.1 → 1.0.3
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 +32 -15
- package/bin/content-grade.js +513 -49
- package/bin/telemetry.js +34 -0
- package/dist-server/server/routes/demos.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,17 +15,27 @@
|
|
|
15
15
|
# 1. Verify Claude CLI is installed and logged in
|
|
16
16
|
claude -p "say hi"
|
|
17
17
|
|
|
18
|
-
# 2. Run
|
|
18
|
+
# 2. Run instant demo — no file needed, see results in ~30 seconds
|
|
19
19
|
npx content-grade
|
|
20
20
|
|
|
21
|
-
# 3. Analyze your own content
|
|
22
|
-
npx content-grade ./my-post.md
|
|
21
|
+
# 3. Analyze your own content (grade is an alias for analyze)
|
|
22
|
+
npx content-grade grade ./my-post.md
|
|
23
|
+
npx content-grade analyze ./my-post.md
|
|
23
24
|
|
|
24
|
-
# 4.
|
|
25
|
+
# 4. CI pipeline — get raw JSON or score-only output
|
|
26
|
+
npx content-grade analyze ./post.md --json
|
|
27
|
+
npx content-grade analyze ./post.md --quiet # prints score number only
|
|
28
|
+
|
|
29
|
+
# 5. Grade a headline
|
|
25
30
|
npx content-grade headline "Why Most Startups Fail at Month 18"
|
|
31
|
+
|
|
32
|
+
# 6. Unlock Pro (batch mode, 100/day limit)
|
|
33
|
+
npx content-grade activate
|
|
26
34
|
```
|
|
27
35
|
|
|
28
|
-
**One requirement:** [Claude CLI](https://claude.ai/code) must be installed and logged in.
|
|
36
|
+
**One requirement:** [Claude CLI](https://claude.ai/code) must be installed and logged in. No API keys, no accounts, no data leaves your machine.
|
|
37
|
+
|
|
38
|
+
**Free tier:** 50 analyses/day, no signup needed. `npx content-grade grade README.md` works immediately.
|
|
29
39
|
|
|
30
40
|
---
|
|
31
41
|
|
|
@@ -72,11 +82,7 @@ npx content-grade headline "Why Most Startups Fail at Month 18"
|
|
|
72
82
|
1. Why 90% of SaaS Startups Fail at Month 18 (and How to Be the 10%)
|
|
73
83
|
2. The Month 18 Startup Trap: What Kills Growth-Stage Companies
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
· Competitor headline comparison
|
|
77
|
-
· Landing page URL audit (full CRO analysis)
|
|
78
|
-
· Ad copy scoring (Google, Meta, LinkedIn)
|
|
79
|
-
· 100 analyses/day vs 3 free
|
|
85
|
+
Unlock team features at contentgrade.dev
|
|
80
86
|
```
|
|
81
87
|
|
|
82
88
|
---
|
|
@@ -86,19 +92,30 @@ npx content-grade headline "Why Most Startups Fail at Month 18"
|
|
|
86
92
|
| Command | What it does |
|
|
87
93
|
|---------|-------------|
|
|
88
94
|
| *(no args)* | Instant demo on built-in sample content |
|
|
95
|
+
| `<url>` | Fetch and analyze a live URL — `npx content-grade https://example.com/blog/post` |
|
|
89
96
|
| `<file>` | Analyze a file directly — `npx content-grade ./post.md` |
|
|
90
97
|
| `.` or `<dir>` | Scan a directory, find and analyze the best content file |
|
|
91
98
|
| `demo` | Same as no args |
|
|
92
99
|
| `analyze <file>` | Full content audit: score, grade, dimensions, improvements |
|
|
100
|
+
| `grade <file>` | Alias for `analyze` (e.g. `grade README.md`) |
|
|
93
101
|
| `check <file>` | Alias for `analyze` |
|
|
94
|
-
| `
|
|
102
|
+
| `batch <dir>` | **[Pro]** Analyze all .md/.txt files in a directory |
|
|
95
103
|
| `headline "<text>"` | Grade a headline on 4 copywriting frameworks |
|
|
96
|
-
| `
|
|
104
|
+
| `activate` | Enter license key to unlock Pro features (batch, 100/day) |
|
|
97
105
|
| `init` | First-run setup: verify Claude CLI, run smoke test |
|
|
98
106
|
| `start` | Launch the full web dashboard (6 tools) |
|
|
99
107
|
| `telemetry [on\|off]` | View or toggle anonymous usage tracking |
|
|
100
108
|
| `help` | Full usage and examples |
|
|
101
109
|
|
|
110
|
+
**Flags:**
|
|
111
|
+
|
|
112
|
+
| Flag | What it does |
|
|
113
|
+
|------|-------------|
|
|
114
|
+
| `--json` | Raw JSON output — pipe to `jq` for CI integration |
|
|
115
|
+
| `--quiet` | Score number only — for shell scripts |
|
|
116
|
+
| `--version` | Print version |
|
|
117
|
+
| `--no-telemetry` | Skip usage tracking for this run |
|
|
118
|
+
|
|
102
119
|
**Global flags:**
|
|
103
120
|
|
|
104
121
|
| Flag | Description |
|
|
@@ -122,7 +139,7 @@ Launch with `content-grade start` — opens at [http://localhost:4000](http://lo
|
|
|
122
139
|
| **EmailForge** | `/email-forge` | Subject line + body copy for click-through optimization |
|
|
123
140
|
| **AudienceDecoder** | `/audience` | Twitter handle → audience archetypes and content patterns |
|
|
124
141
|
|
|
125
|
-
Free tier: **
|
|
142
|
+
Free tier: **50 analyses/day per tool**. Pro ($9/mo): **100 analyses/day** + all tools.
|
|
126
143
|
|
|
127
144
|
---
|
|
128
145
|
|
|
@@ -537,8 +554,8 @@ curl -s -X POST http://localhost:4000/api/demos/email-forge \
|
|
|
537
554
|
"gated": true,
|
|
538
555
|
"isPro": false,
|
|
539
556
|
"remaining": 0,
|
|
540
|
-
"limit":
|
|
541
|
-
"message": "Free daily limit reached (
|
|
557
|
+
"limit": 50,
|
|
558
|
+
"message": "Free daily limit reached (50/day). Get 100 analyses/day with Pro"
|
|
542
559
|
}
|
|
543
560
|
```
|
|
544
561
|
|
package/bin/content-grade.js
CHANGED
|
@@ -16,14 +16,65 @@
|
|
|
16
16
|
import { execFileSync, execFile, spawn } from 'child_process';
|
|
17
17
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, statSync } from 'fs';
|
|
18
18
|
import { resolve, dirname, basename, extname } from 'path';
|
|
19
|
+
import { homedir } from 'os';
|
|
19
20
|
import { fileURLToPath } from 'url';
|
|
20
21
|
import { promisify } from 'util';
|
|
22
|
+
import { get as httpsGet } from 'https';
|
|
23
|
+
import { get as httpGet } from 'http';
|
|
21
24
|
import { initTelemetry, recordEvent, disableTelemetry, enableTelemetry, telemetryStatus, isOptedOut } from './telemetry.js';
|
|
22
25
|
|
|
23
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
27
|
const root = resolve(__dirname, '..');
|
|
25
28
|
const execFileAsync = promisify(execFile);
|
|
26
29
|
|
|
30
|
+
// ── Config + license ──────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const CONFIG_DIR = resolve(homedir(), '.config', 'content-grade');
|
|
33
|
+
const CONFIG_FILE = resolve(CONFIG_DIR, 'config.json');
|
|
34
|
+
const USAGE_FILE = resolve(CONFIG_DIR, 'usage.json');
|
|
35
|
+
|
|
36
|
+
function loadConfig() {
|
|
37
|
+
try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function saveConfig(data) {
|
|
41
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
42
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getLicenseKey() {
|
|
46
|
+
return process.env.CONTENT_GRADE_KEY || loadConfig().licenseKey || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isProUser() { return Boolean(getLicenseKey()); }
|
|
50
|
+
|
|
51
|
+
function getTodayKey() { return new Date().toISOString().slice(0, 10); }
|
|
52
|
+
|
|
53
|
+
function getUsage() {
|
|
54
|
+
try {
|
|
55
|
+
const d = JSON.parse(readFileSync(USAGE_FILE, 'utf8'));
|
|
56
|
+
if (d.date !== getTodayKey()) return { date: getTodayKey(), count: 0 };
|
|
57
|
+
return d;
|
|
58
|
+
} catch { return { date: getTodayKey(), count: 0 }; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function incrementUsage() {
|
|
62
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
63
|
+
const u = getUsage();
|
|
64
|
+
u.count = (u.count || 0) + 1;
|
|
65
|
+
writeFileSync(USAGE_FILE, JSON.stringify(u, null, 2), 'utf8');
|
|
66
|
+
return u.count;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const FREE_DAILY_LIMIT = 50;
|
|
70
|
+
|
|
71
|
+
function checkDailyLimit() {
|
|
72
|
+
if (isProUser()) return { ok: true };
|
|
73
|
+
const u = getUsage();
|
|
74
|
+
if (u.count >= FREE_DAILY_LIMIT) return { ok: false, count: u.count, limit: FREE_DAILY_LIMIT };
|
|
75
|
+
return { ok: true, count: u.count, limit: FREE_DAILY_LIMIT };
|
|
76
|
+
}
|
|
77
|
+
|
|
27
78
|
let _version = '1.0.0';
|
|
28
79
|
try { _version = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')).version ?? '1.0.0'; } catch {}
|
|
29
80
|
|
|
@@ -99,11 +150,27 @@ async function askClaude(prompt, systemPrompt, model = 'claude-haiku-4-5-2025100
|
|
|
99
150
|
if (systemPrompt) args.push('--system-prompt', systemPrompt);
|
|
100
151
|
args.push(prompt);
|
|
101
152
|
|
|
153
|
+
if (_verboseMode) {
|
|
154
|
+
blank();
|
|
155
|
+
console.log(` ${D}[verbose] model: ${model}${R}`);
|
|
156
|
+
console.log(` ${D}[verbose] claude: ${claudePath}${R}`);
|
|
157
|
+
console.log(` ${D}[verbose] prompt length: ${prompt.length} chars${R}`);
|
|
158
|
+
blank();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const t0 = Date.now();
|
|
102
162
|
const { stdout } = await execFileAsync(claudePath, args, {
|
|
103
163
|
timeout: 120000,
|
|
104
164
|
maxBuffer: 10 * 1024 * 1024,
|
|
105
165
|
});
|
|
106
|
-
|
|
166
|
+
const raw = stdout.trim();
|
|
167
|
+
|
|
168
|
+
if (_verboseMode) {
|
|
169
|
+
console.log(` ${D}[verbose] response: ${raw.length} chars in ${Date.now() - t0}ms${R}`);
|
|
170
|
+
blank();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return raw;
|
|
107
174
|
}
|
|
108
175
|
|
|
109
176
|
function parseJSON(raw) {
|
|
@@ -187,7 +254,7 @@ async function cmdAnalyze(filePath) {
|
|
|
187
254
|
blank();
|
|
188
255
|
console.log(` ${D}No file? Try the demo: ${CY}content-grade demo${R}`);
|
|
189
256
|
blank();
|
|
190
|
-
process.exit(
|
|
257
|
+
process.exit(2);
|
|
191
258
|
}
|
|
192
259
|
|
|
193
260
|
const absPath = resolve(process.cwd(), filePath);
|
|
@@ -198,7 +265,7 @@ async function cmdAnalyze(filePath) {
|
|
|
198
265
|
console.log(` ${YL}Check the path and try again.${R}`);
|
|
199
266
|
console.log(` ${D}Tip: use a relative path like ./my-file.md or an absolute path.${R}`);
|
|
200
267
|
blank();
|
|
201
|
-
process.exit(
|
|
268
|
+
process.exit(2);
|
|
202
269
|
}
|
|
203
270
|
|
|
204
271
|
// Guard: reject directories
|
|
@@ -209,7 +276,7 @@ async function cmdAnalyze(filePath) {
|
|
|
209
276
|
blank();
|
|
210
277
|
console.log(` ${D}Pass a text file (.md, .txt) — not a directory.${R}`);
|
|
211
278
|
blank();
|
|
212
|
-
process.exit(
|
|
279
|
+
process.exit(2);
|
|
213
280
|
}
|
|
214
281
|
|
|
215
282
|
// Guard: reject files over 500 KB before reading into memory
|
|
@@ -220,7 +287,7 @@ async function cmdAnalyze(filePath) {
|
|
|
220
287
|
blank();
|
|
221
288
|
console.log(` ${YL}Tip:${R} Copy the relevant section into a new file and analyze that.`);
|
|
222
289
|
blank();
|
|
223
|
-
process.exit(
|
|
290
|
+
process.exit(2);
|
|
224
291
|
}
|
|
225
292
|
|
|
226
293
|
const content = readFileSync(absPath, 'utf8');
|
|
@@ -233,20 +300,35 @@ async function cmdAnalyze(filePath) {
|
|
|
233
300
|
console.log(` ${YL}ContentGrade analyzes written content — blog posts, emails, ad copy, landing pages.${R}`);
|
|
234
301
|
console.log(` ${D}Supported formats: .md, .txt, .mdx, or any plain-text file.${R}`);
|
|
235
302
|
blank();
|
|
236
|
-
process.exit(
|
|
303
|
+
process.exit(2);
|
|
237
304
|
}
|
|
238
305
|
|
|
239
306
|
if (content.trim().length < 20) {
|
|
240
307
|
blank();
|
|
241
308
|
fail(`File is too short to analyze (${content.trim().length} chars). Add some content and try again.`);
|
|
242
309
|
blank();
|
|
310
|
+
process.exit(2);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Free tier daily limit
|
|
314
|
+
const limitCheck = checkDailyLimit();
|
|
315
|
+
if (!limitCheck.ok) {
|
|
316
|
+
blank();
|
|
317
|
+
fail(`Daily limit reached (${limitCheck.count}/${limitCheck.limit} free checks used today).`);
|
|
318
|
+
blank();
|
|
319
|
+
console.log(` ${B}Options:${R}`);
|
|
320
|
+
console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
|
|
321
|
+
console.log(` ${D}· Unlock 100 checks/day: ${CY}content-grade activate${R}`);
|
|
322
|
+
blank();
|
|
243
323
|
process.exit(1);
|
|
244
324
|
}
|
|
245
325
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
326
|
+
if (!_jsonMode && !_quietMode) {
|
|
327
|
+
banner();
|
|
328
|
+
console.log(` ${B}Analyzing:${R} ${CY}${basename(absPath)}${R}`);
|
|
329
|
+
console.log(` ${D}${content.length.toLocaleString()} characters · detecting content type...${R}`);
|
|
330
|
+
blank();
|
|
331
|
+
}
|
|
250
332
|
|
|
251
333
|
// Check Claude
|
|
252
334
|
if (!checkClaude()) {
|
|
@@ -271,7 +353,7 @@ async function cmdAnalyze(filePath) {
|
|
|
271
353
|
|
|
272
354
|
const truncated = content.length > 6000 ? content.slice(0, 6000) + '\n\n[Content truncated for analysis]' : content;
|
|
273
355
|
|
|
274
|
-
process.stdout.write(` ${D}Running analysis (this takes ~15 seconds)...${R}`);
|
|
356
|
+
if (!_jsonMode && !_quietMode) process.stdout.write(` ${D}Running analysis (this takes ~15 seconds)...${R}`);
|
|
275
357
|
|
|
276
358
|
let result;
|
|
277
359
|
try {
|
|
@@ -280,9 +362,12 @@ async function cmdAnalyze(filePath) {
|
|
|
280
362
|
ANALYZE_SYSTEM,
|
|
281
363
|
'claude-sonnet-4-6'
|
|
282
364
|
);
|
|
283
|
-
process.stdout.write(`\r ${GN}✓${R} Analysis complete${' '.repeat(30)}\n`);
|
|
365
|
+
if (!_jsonMode && !_quietMode) process.stdout.write(`\r ${GN}✓${R} Analysis complete${' '.repeat(30)}\n`);
|
|
284
366
|
result = parseJSON(raw);
|
|
285
367
|
recordEvent({ event: 'analyze_result', score: result.total_score, content_type: result.content_type });
|
|
368
|
+
// Machine-readable output modes (exit cleanly, skip styled output)
|
|
369
|
+
if (_jsonMode) { process.stdout.write(JSON.stringify(result, null, 2) + '\n'); return; }
|
|
370
|
+
if (_quietMode) { process.stdout.write(`${result.total_score}\n`); return; }
|
|
286
371
|
} catch (err) {
|
|
287
372
|
process.stdout.write(`\n`);
|
|
288
373
|
blank();
|
|
@@ -373,18 +458,22 @@ async function cmdAnalyze(filePath) {
|
|
|
373
458
|
blank();
|
|
374
459
|
}
|
|
375
460
|
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
console.log(` ${D} · Landing page URL audit (full CRO analysis)${R}`);
|
|
381
|
-
console.log(` ${D} · Ad copy scoring (Google, Meta, LinkedIn)${R}`);
|
|
382
|
-
console.log(` ${D} · Twitter thread grader with shareability score${R}`);
|
|
383
|
-
console.log(` ${D} · Email subject line optimizer${R}`);
|
|
384
|
-
console.log(` ${D} · 100 analyses/day vs 3 free${R}`);
|
|
461
|
+
// Track usage
|
|
462
|
+
incrementUsage();
|
|
463
|
+
|
|
464
|
+
// Upsell — show Pro path after user has seen value
|
|
385
465
|
blank();
|
|
386
|
-
|
|
387
|
-
|
|
466
|
+
if (isProUser()) {
|
|
467
|
+
console.log(` ${D}Pro active · ${CY}contentgrade.dev${R}`);
|
|
468
|
+
} else {
|
|
469
|
+
const usage = getUsage();
|
|
470
|
+
const remaining = Math.max(0, FREE_DAILY_LIMIT - usage.count);
|
|
471
|
+
hr();
|
|
472
|
+
console.log(` ${MG}${B}Unlock batch mode:${R} ${CY}content-grade activate${R}`);
|
|
473
|
+
console.log(` ${D} · Analyze entire directories in one command${R}`);
|
|
474
|
+
console.log(` ${D} · 100 checks/day (${remaining} remaining today on free tier)${R}`);
|
|
475
|
+
console.log(` ${D} · Get a license at ${CY}contentgrade.dev${R}`);
|
|
476
|
+
}
|
|
388
477
|
blank();
|
|
389
478
|
}
|
|
390
479
|
|
|
@@ -424,6 +513,19 @@ async function cmdHeadline(text) {
|
|
|
424
513
|
console.log(` ${D}content-grade headline "How We Grew From 0 to 10k Users Without Ads"${R}`);
|
|
425
514
|
console.log(` ${D}content-grade headline "Stop Doing This One Thing in Your Email Subject Lines"${R}`);
|
|
426
515
|
blank();
|
|
516
|
+
process.exit(2);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Free tier daily limit
|
|
520
|
+
const limitCheck = checkDailyLimit();
|
|
521
|
+
if (!limitCheck.ok) {
|
|
522
|
+
blank();
|
|
523
|
+
fail(`Daily limit reached (${limitCheck.count}/${limitCheck.limit} free checks used today).`);
|
|
524
|
+
blank();
|
|
525
|
+
console.log(` ${B}Options:${R}`);
|
|
526
|
+
console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
|
|
527
|
+
console.log(` ${D}· Unlock 100 checks/day: ${CY}content-grade activate${R}`);
|
|
528
|
+
blank();
|
|
427
529
|
process.exit(1);
|
|
428
530
|
}
|
|
429
531
|
|
|
@@ -579,8 +681,209 @@ async function cmdInit() {
|
|
|
579
681
|
console.log(` ${CY}content-grade start${R}`);
|
|
580
682
|
blank();
|
|
581
683
|
}
|
|
582
|
-
console.log(` ${D}Pro tier ($9/mo): 100 analyses/day + competitor comparison + URL audits${R}`);
|
|
684
|
+
console.log(` ${D}Pro tier ($9/mo): 100 analyses/day (vs 50 free) + competitor comparison + URL audits${R}`);
|
|
685
|
+
blank();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ── Activate command ──────────────────────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
async function cmdActivate() {
|
|
691
|
+
banner();
|
|
692
|
+
console.log(` ${B}Activate ContentGrade Pro${R}`);
|
|
693
|
+
blank();
|
|
694
|
+
|
|
695
|
+
const existing = getLicenseKey();
|
|
696
|
+
if (existing) {
|
|
697
|
+
ok(`Pro license already activated`);
|
|
698
|
+
blank();
|
|
699
|
+
console.log(` ${D}Key: ${existing.slice(0, 8)}...${R}`);
|
|
700
|
+
console.log(` ${D}Config: ${CONFIG_FILE}${R}`);
|
|
701
|
+
blank();
|
|
702
|
+
console.log(` ${B}Pro features:${R}`);
|
|
703
|
+
console.log(` ${D} content-grade batch ./posts/ ${R}${D}# analyze all files in a directory${R}`);
|
|
704
|
+
console.log(` ${D} 100 checks/day (vs 50 free)${R}`);
|
|
705
|
+
blank();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
console.log(` ${D}Pro tier unlocks:${R}`);
|
|
710
|
+
console.log(` ${D} · ${CY}content-grade batch <dir>${R}${D} — analyze entire directories${R}`);
|
|
711
|
+
console.log(` ${D} · 100 checks/day (vs 50 free)${R}`);
|
|
712
|
+
blank();
|
|
713
|
+
console.log(` ${D}Get a license: ${CY}contentgrade.dev${R}${D} → Pricing → Team${R}`);
|
|
714
|
+
blank();
|
|
715
|
+
|
|
716
|
+
process.stdout.write(` ${CY}License key:${R} `);
|
|
717
|
+
|
|
718
|
+
const key = await new Promise(res => {
|
|
719
|
+
let input = '';
|
|
720
|
+
const isRaw = process.stdin.isTTY;
|
|
721
|
+
if (isRaw) process.stdin.setRawMode(true);
|
|
722
|
+
process.stdin.resume();
|
|
723
|
+
process.stdin.setEncoding('utf8');
|
|
724
|
+
process.stdin.on('data', function onData(chunk) {
|
|
725
|
+
if (chunk === '\r' || chunk === '\n') {
|
|
726
|
+
if (isRaw) process.stdin.setRawMode(false);
|
|
727
|
+
process.stdin.pause();
|
|
728
|
+
process.stdin.removeListener('data', onData);
|
|
729
|
+
process.stdout.write('\n');
|
|
730
|
+
res(input.trim());
|
|
731
|
+
} else if (chunk === '\u0003') {
|
|
732
|
+
if (isRaw) process.stdin.setRawMode(false);
|
|
733
|
+
process.stdin.pause();
|
|
734
|
+
process.stdout.write('\n');
|
|
735
|
+
process.exit(0);
|
|
736
|
+
} else if (chunk === '\u007f') {
|
|
737
|
+
if (input.length > 0) { input = input.slice(0, -1); process.stdout.write('\b \b'); }
|
|
738
|
+
} else {
|
|
739
|
+
input += chunk;
|
|
740
|
+
process.stdout.write('*');
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
if (!key || key.length < 8) {
|
|
746
|
+
blank();
|
|
747
|
+
fail(`Invalid key — must be at least 8 characters.`);
|
|
748
|
+
blank();
|
|
749
|
+
process.exit(2);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const config = loadConfig();
|
|
753
|
+
config.licenseKey = key;
|
|
754
|
+
config.activatedAt = new Date().toISOString();
|
|
755
|
+
saveConfig(config);
|
|
756
|
+
|
|
757
|
+
blank();
|
|
758
|
+
ok(`License activated! Pro features unlocked.`);
|
|
759
|
+
blank();
|
|
760
|
+
console.log(` ${B}Try it:${R}`);
|
|
761
|
+
console.log(` ${CY} content-grade batch ./posts/${R} ${D}# analyze all files${R}`);
|
|
762
|
+
blank();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ── Batch command (Pro) ───────────────────────────────────────────────────────
|
|
766
|
+
|
|
767
|
+
async function cmdBatch(dirPath) {
|
|
768
|
+
if (!isProUser()) {
|
|
769
|
+
blank();
|
|
770
|
+
console.log(` ${B}${MG}Batch Analysis — Pro Feature${R}`);
|
|
771
|
+
blank();
|
|
772
|
+
console.log(` ${D}Batch mode requires a Pro license.${R}`);
|
|
773
|
+
blank();
|
|
774
|
+
console.log(` ${D}Free tier: analyze files one at a time.${R}`);
|
|
775
|
+
console.log(` ${CY}content-grade analyze ./post.md${R}`);
|
|
776
|
+
blank();
|
|
777
|
+
console.log(` ${B}Unlock batch mode:${R}`);
|
|
778
|
+
console.log(` ${CY}content-grade activate${R} ${D}(enter your license key)${R}`);
|
|
779
|
+
blank();
|
|
780
|
+
console.log(` ${D}Get a license: ${CY}contentgrade.dev${R}`);
|
|
781
|
+
blank();
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (!dirPath) {
|
|
786
|
+
blank();
|
|
787
|
+
fail(`No directory specified.`);
|
|
788
|
+
blank();
|
|
789
|
+
console.log(` Usage: ${CY}content-grade batch <directory>${R}`);
|
|
790
|
+
console.log(` Example: ${CY}content-grade batch ./posts${R}`);
|
|
791
|
+
blank();
|
|
792
|
+
process.exit(2);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const absDir = resolve(process.cwd(), dirPath);
|
|
796
|
+
if (!existsSync(absDir)) {
|
|
797
|
+
blank();
|
|
798
|
+
fail(`Directory not found: ${absDir}`);
|
|
799
|
+
blank();
|
|
800
|
+
process.exit(2);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const st = statSync(absDir);
|
|
804
|
+
if (!st.isDirectory()) {
|
|
805
|
+
blank();
|
|
806
|
+
fail(`${dirPath} is not a directory. Use ${CY}content-grade analyze${R} for single files.`);
|
|
807
|
+
blank();
|
|
808
|
+
process.exit(2);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const files = [];
|
|
812
|
+
function collectFiles(dir) {
|
|
813
|
+
let entries;
|
|
814
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
815
|
+
for (const entry of entries) {
|
|
816
|
+
if (entry.startsWith('.') || entry === 'node_modules' || entry === 'dist') continue;
|
|
817
|
+
const full = resolve(dir, entry);
|
|
818
|
+
let s;
|
|
819
|
+
try { s = statSync(full); } catch { continue; }
|
|
820
|
+
if (s.isDirectory()) collectFiles(full);
|
|
821
|
+
else if (/\.(md|txt|mdx)$/i.test(entry)) files.push(full);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
collectFiles(absDir);
|
|
825
|
+
|
|
826
|
+
if (!files.length) {
|
|
827
|
+
blank();
|
|
828
|
+
fail(`No .md, .txt, or .mdx files found in ${dirPath}`);
|
|
829
|
+
blank();
|
|
830
|
+
process.exit(1);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (!checkClaude()) {
|
|
834
|
+
fail(`Claude CLI not found. Run: ${CY}content-grade init${R}`);
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
banner();
|
|
839
|
+
console.log(` ${B}Batch Analysis${R} ${D}${files.length} files in ${dirPath}${R}`);
|
|
583
840
|
blank();
|
|
841
|
+
|
|
842
|
+
const results = [];
|
|
843
|
+
for (let i = 0; i < files.length; i++) {
|
|
844
|
+
const f = files[i];
|
|
845
|
+
const rel = f.startsWith(process.cwd()) ? f.slice(process.cwd().length + 1) : f;
|
|
846
|
+
process.stdout.write(` ${D}[${i + 1}/${files.length}]${R} ${rel}...`);
|
|
847
|
+
|
|
848
|
+
let content;
|
|
849
|
+
try { content = readFileSync(f, 'utf8'); } catch { process.stdout.write(` ${RD}unreadable${R}\n`); continue; }
|
|
850
|
+
if (content.trim().length < 20 || content.includes('\x00')) { process.stdout.write(` ${YL}skipped${R}\n`); continue; }
|
|
851
|
+
|
|
852
|
+
try {
|
|
853
|
+
const truncated = content.length > 6000 ? content.slice(0, 6000) + '\n\n[truncated]' : content;
|
|
854
|
+
const raw = await askClaude(
|
|
855
|
+
`Analyze this content:\n---\n${truncated}\n---`,
|
|
856
|
+
ANALYZE_SYSTEM,
|
|
857
|
+
'claude-haiku-4-5-20251001'
|
|
858
|
+
);
|
|
859
|
+
const r = parseJSON(raw);
|
|
860
|
+
incrementUsage();
|
|
861
|
+
const sc = r.total_score;
|
|
862
|
+
const scoreColor = sc >= 70 ? GN : sc >= 45 ? YL : RD;
|
|
863
|
+
process.stdout.write(`\r ${scoreColor}${String(sc).padStart(3)}/100${R} ${gradeLetter(sc)} ${rel}${' '.repeat(10)}\n`);
|
|
864
|
+
results.push({ file: rel, score: sc, grade: r.grade });
|
|
865
|
+
} catch { process.stdout.write(`\r ${RD}failed${R} ${rel}\n`); }
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
blank();
|
|
869
|
+
hr();
|
|
870
|
+
console.log(` ${B}BATCH SUMMARY${R} ${D}${results.length}/${files.length} analyzed${R}`);
|
|
871
|
+
blank();
|
|
872
|
+
|
|
873
|
+
results.sort((a, b) => b.score - a.score);
|
|
874
|
+
for (const r of results) {
|
|
875
|
+
const scoreColor = r.score >= 70 ? GN : r.score >= 45 ? YL : RD;
|
|
876
|
+
console.log(` ${scoreColor}${String(r.score).padStart(3)}${R} ${gradeLetter(r.score)} ${r.file}`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (results.length) {
|
|
880
|
+
const avg = Math.round(results.reduce((sum, r) => sum + r.score, 0) / results.length);
|
|
881
|
+
blank();
|
|
882
|
+
console.log(` ${D}Average score: ${avg}/100${R}`);
|
|
883
|
+
}
|
|
884
|
+
blank();
|
|
885
|
+
|
|
886
|
+
if (_jsonMode) process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
584
887
|
}
|
|
585
888
|
|
|
586
889
|
// ── Start command ─────────────────────────────────────────────────────────────
|
|
@@ -657,7 +960,7 @@ function cmdStart() {
|
|
|
657
960
|
info(` EmailForge — ${url}/email-forge`);
|
|
658
961
|
info(` AudienceDecoder — ${url}/audience`);
|
|
659
962
|
blank();
|
|
660
|
-
info(`Free tier:
|
|
963
|
+
info(`Free tier: 50 analyses/day. Upgrade at ${url}`);
|
|
661
964
|
info(`Press Ctrl+C to stop`);
|
|
662
965
|
blank();
|
|
663
966
|
openBrowser(url);
|
|
@@ -714,28 +1017,36 @@ function cmdHelp() {
|
|
|
714
1017
|
|
|
715
1018
|
console.log(` ${B}COMMANDS${R}`);
|
|
716
1019
|
blank();
|
|
717
|
-
console.log(` ${CY}demo${R}
|
|
1020
|
+
console.log(` ${CY}demo${R} Run on sample content — instant result, no file needed`);
|
|
718
1021
|
blank();
|
|
719
|
-
console.log(` ${CY}analyze <file>${R}
|
|
720
|
-
console.log(` ${CY}
|
|
721
|
-
console.log(` ${
|
|
722
|
-
console.log(` ${D}
|
|
1022
|
+
console.log(` ${CY}analyze <file>${R} Analyze content from a file`);
|
|
1023
|
+
console.log(` ${CY}grade <file>${R} Same as analyze (alias)`);
|
|
1024
|
+
console.log(` ${CY}check <file>${R} Same as analyze (alias)`);
|
|
1025
|
+
console.log(` ${D} Accepts .md, .txt, .mdx or any plain-text file${R}`);
|
|
1026
|
+
console.log(` ${D} Zero config — just needs Claude CLI${R}`);
|
|
723
1027
|
blank();
|
|
724
|
-
console.log(` ${CY}
|
|
725
|
-
console.log(` ${D} Scores on 4 copywriting frameworks${R}`);
|
|
1028
|
+
console.log(` ${CY}batch <directory>${R} ${MG}[Pro]${R} Analyze all .md/.txt files in a directory`);
|
|
726
1029
|
blank();
|
|
727
|
-
console.log(` ${CY}
|
|
728
|
-
console.log(` ${D} 6 tools: headlines, pages, ads, threads...${R}`);
|
|
1030
|
+
console.log(` ${CY}headline "<text>"${R} Grade a single headline (4 copywriting frameworks)`);
|
|
729
1031
|
blank();
|
|
730
|
-
console.log(` ${CY}
|
|
1032
|
+
console.log(` ${CY}activate${R} Enter license key to unlock Pro features`);
|
|
731
1033
|
blank();
|
|
732
|
-
console.log(` ${CY}
|
|
1034
|
+
console.log(` ${CY}start${R} Launch the full web dashboard`);
|
|
1035
|
+
console.log(` ${D} 6 tools: headlines, pages, ads, threads, emails, audiences${R}`);
|
|
733
1036
|
blank();
|
|
734
|
-
console.log(` ${CY}
|
|
1037
|
+
console.log(` ${CY}init${R} First-run setup and diagnostics`);
|
|
1038
|
+
blank();
|
|
1039
|
+
console.log(` ${CY}telemetry [on|off]${R} View or toggle anonymous usage tracking`);
|
|
1040
|
+
blank();
|
|
1041
|
+
console.log(` ${CY}help${R} Show this help`);
|
|
735
1042
|
blank();
|
|
736
1043
|
console.log(` ${B}FLAGS${R}`);
|
|
737
1044
|
blank();
|
|
738
|
-
console.log(` ${CY}--
|
|
1045
|
+
console.log(` ${CY}--json${R} Output raw JSON (great for CI pipelines)`);
|
|
1046
|
+
console.log(` ${CY}--quiet${R} Output score number only (for scripting)`);
|
|
1047
|
+
console.log(` ${CY}--verbose${R} Show debug info: model, timing, raw response length`);
|
|
1048
|
+
console.log(` ${CY}--version${R} Print version and exit`);
|
|
1049
|
+
console.log(` ${CY}--no-telemetry${R} Skip usage tracking for this invocation`);
|
|
739
1050
|
blank();
|
|
740
1051
|
|
|
741
1052
|
console.log(` ${B}EXAMPLES${R}`);
|
|
@@ -743,9 +1054,21 @@ function cmdHelp() {
|
|
|
743
1054
|
console.log(` ${D}# Analyze a blog post${R}`);
|
|
744
1055
|
console.log(` ${CY}content-grade analyze ./my-post.md${R}`);
|
|
745
1056
|
blank();
|
|
1057
|
+
console.log(` ${D}# Grade README.md (alias)${R}`);
|
|
1058
|
+
console.log(` ${CY}content-grade grade README.md${R}`);
|
|
1059
|
+
blank();
|
|
1060
|
+
console.log(` ${D}# CI pipeline — exit 1 if score < threshold${R}`);
|
|
1061
|
+
console.log(` ${CY}content-grade analyze ./blog.md --json | jq '.total_score'${R}`);
|
|
1062
|
+
blank();
|
|
1063
|
+
console.log(` ${D}# Get score number only (for scripts)${R}`);
|
|
1064
|
+
console.log(` ${CY}content-grade analyze ./post.md --quiet${R}`);
|
|
1065
|
+
blank();
|
|
746
1066
|
console.log(` ${D}# Grade a headline${R}`);
|
|
747
1067
|
console.log(` ${CY}content-grade headline "How I 10x'd My Conversion Rate in 30 Days"${R}`);
|
|
748
1068
|
blank();
|
|
1069
|
+
console.log(` ${D}# Batch analyze a content directory (Pro)${R}`);
|
|
1070
|
+
console.log(` ${CY}content-grade batch ./posts${R}`);
|
|
1071
|
+
blank();
|
|
749
1072
|
console.log(` ${D}# Launch full dashboard${R}`);
|
|
750
1073
|
console.log(` ${CY}content-grade start${R}`);
|
|
751
1074
|
blank();
|
|
@@ -758,7 +1081,7 @@ function cmdHelp() {
|
|
|
758
1081
|
|
|
759
1082
|
console.log(` ${B}PRO TIER${R} ${MG}$9/month${R}`);
|
|
760
1083
|
blank();
|
|
761
|
-
console.log(` · 100 analyses/day (vs
|
|
1084
|
+
console.log(` · 100 analyses/day (vs 50 free)`);
|
|
762
1085
|
console.log(` · Competitor headline A/B comparison`);
|
|
763
1086
|
console.log(` · Landing page URL audit`);
|
|
764
1087
|
console.log(` · Ad copy scoring (Google, Meta, LinkedIn)`);
|
|
@@ -787,6 +1110,18 @@ In conclusion, these productivity tips are very helpful. Try them today and you
|
|
|
787
1110
|
`;
|
|
788
1111
|
|
|
789
1112
|
async function cmdDemo() {
|
|
1113
|
+
// If Claude isn't installed, skip the demo and run guided setup instead
|
|
1114
|
+
if (!checkClaude()) {
|
|
1115
|
+
console.log('');
|
|
1116
|
+
console.log(` ${B}${CY}Welcome to ContentGrade${R}`);
|
|
1117
|
+
console.log('');
|
|
1118
|
+
console.log(` ${D}ContentGrade needs Claude CLI to run analysis.${R}`);
|
|
1119
|
+
console.log(` ${D}Let's get you set up — it only takes a minute.${R}`);
|
|
1120
|
+
console.log('');
|
|
1121
|
+
await cmdInit();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
790
1125
|
const tmpFile = `/tmp/content-grade-demo-${process.pid}-${Date.now()}.md`;
|
|
791
1126
|
|
|
792
1127
|
banner();
|
|
@@ -799,6 +1134,107 @@ async function cmdDemo() {
|
|
|
799
1134
|
blank();
|
|
800
1135
|
|
|
801
1136
|
writeFileSync(tmpFile, DEMO_CONTENT, 'utf8');
|
|
1137
|
+
try {
|
|
1138
|
+
await cmdAnalyze(tmpFile);
|
|
1139
|
+
// Show telemetry notice after user has seen value (first run only)
|
|
1140
|
+
if (_showTelemNotice) {
|
|
1141
|
+
console.log(` ${D}Anonymous usage data is collected by default. Opt out: ${CY}content-grade telemetry off${R}`);
|
|
1142
|
+
blank();
|
|
1143
|
+
}
|
|
1144
|
+
} finally {
|
|
1145
|
+
try { unlinkSync(tmpFile); } catch {}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// ── URL fetcher ───────────────────────────────────────────────────────────────
|
|
1150
|
+
|
|
1151
|
+
function fetchUrl(url) {
|
|
1152
|
+
return new Promise((resolve, reject) => {
|
|
1153
|
+
const get = url.startsWith('https') ? httpsGet : httpGet;
|
|
1154
|
+
const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/StanislavBG/Content-Grade)' }, timeout: 15000 }, (res) => {
|
|
1155
|
+
// Follow one redirect
|
|
1156
|
+
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
|
|
1157
|
+
fetchUrl(res.headers.location).then(resolve).catch(reject);
|
|
1158
|
+
res.resume();
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
1162
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
1163
|
+
res.resume();
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const chunks = [];
|
|
1167
|
+
res.on('data', c => chunks.push(c));
|
|
1168
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8', 0, 500000)));
|
|
1169
|
+
res.on('error', reject);
|
|
1170
|
+
});
|
|
1171
|
+
req.on('error', reject);
|
|
1172
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function htmlToText(html) {
|
|
1177
|
+
return html
|
|
1178
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
1179
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
1180
|
+
.replace(/<nav[\s\S]*?<\/nav>/gi, ' ')
|
|
1181
|
+
.replace(/<footer[\s\S]*?<\/footer>/gi, ' ')
|
|
1182
|
+
.replace(/<header[\s\S]*?<\/header>/gi, ' ')
|
|
1183
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
1184
|
+
.replace(/<(h[1-6])[^>]*>([\s\S]*?)<\/\1>/gi, (_, _t, txt) => `\n\n## ${txt}\n\n`)
|
|
1185
|
+
.replace(/<(p|li|blockquote)[^>]*>([\s\S]*?)<\/\1>/gi, (_, _t, txt) => `\n${txt}\n`)
|
|
1186
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
1187
|
+
.replace(/<[^>]+>/g, ' ')
|
|
1188
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ')
|
|
1189
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
1190
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
1191
|
+
.trim();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
async function cmdAnalyzeUrl(url) {
|
|
1195
|
+
banner();
|
|
1196
|
+
console.log(` ${B}Analyzing URL:${R} ${CY}${url}${R}`);
|
|
1197
|
+
blank();
|
|
1198
|
+
|
|
1199
|
+
if (!checkClaude()) {
|
|
1200
|
+
fail(`Claude CLI not found.`);
|
|
1201
|
+
blank();
|
|
1202
|
+
console.log(` ContentGrade uses your local Claude CLI — no API keys needed.`);
|
|
1203
|
+
console.log(` Install from ${CY}https://claude.ai/code${R} then run ${CY}claude login${R}`);
|
|
1204
|
+
blank();
|
|
1205
|
+
process.exit(1);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
process.stdout.write(` ${D}Fetching page...${R}`);
|
|
1209
|
+
let html;
|
|
1210
|
+
try {
|
|
1211
|
+
html = await fetchUrl(url);
|
|
1212
|
+
process.stdout.write(`\r ${GN}✓${R} Page fetched${' '.repeat(20)}\n`);
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
process.stdout.write(`\n`);
|
|
1215
|
+
blank();
|
|
1216
|
+
fail(`Could not fetch URL: ${err.message}`);
|
|
1217
|
+
blank();
|
|
1218
|
+
console.log(` ${YL}Check:${R}`);
|
|
1219
|
+
console.log(` ${D}· URL is accessible (try opening it in a browser)${R}`);
|
|
1220
|
+
console.log(` ${D}· You have internet access${R}`);
|
|
1221
|
+
blank();
|
|
1222
|
+
process.exit(1);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const text = htmlToText(html);
|
|
1226
|
+
if (text.length < 50) {
|
|
1227
|
+
blank();
|
|
1228
|
+
fail(`Could not extract readable content from ${url}`);
|
|
1229
|
+
blank();
|
|
1230
|
+
console.log(` ${D}The page may be JavaScript-rendered. Try saving the page content to a .md file and running:${R}`);
|
|
1231
|
+
console.log(` ${CY} content-grade analyze ./page.md${R}`);
|
|
1232
|
+
blank();
|
|
1233
|
+
process.exit(1);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const tmpFile = `/tmp/content-grade-url-${process.pid}-${Date.now()}.md`;
|
|
1237
|
+
writeFileSync(tmpFile, `# ${url}\n\n${text}`, 'utf8');
|
|
802
1238
|
try {
|
|
803
1239
|
await cmdAnalyze(tmpFile);
|
|
804
1240
|
} finally {
|
|
@@ -853,19 +1289,18 @@ function findBestContentFile(dirPath) {
|
|
|
853
1289
|
|
|
854
1290
|
// ── Router ────────────────────────────────────────────────────────────────────
|
|
855
1291
|
|
|
856
|
-
const
|
|
1292
|
+
const _rawArgs = process.argv.slice(2);
|
|
1293
|
+
const _jsonMode = _rawArgs.includes('--json');
|
|
1294
|
+
const _quietMode = _rawArgs.includes('--quiet');
|
|
1295
|
+
const _verboseMode = _rawArgs.includes('--verbose') || _rawArgs.includes('-v') && !_rawArgs.includes('--version');
|
|
1296
|
+
const args = _rawArgs.filter(a => !['--no-telemetry', '--json', '--quiet', '--verbose'].includes(a));
|
|
857
1297
|
const raw = args[0];
|
|
858
1298
|
const cmd = raw?.toLowerCase();
|
|
859
1299
|
|
|
860
1300
|
// ── Telemetry init ────────────────────────────────────────────────────────────
|
|
861
1301
|
const _telem = initTelemetry();
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
blank();
|
|
865
|
-
console.log(` ${D}ContentGrade collects anonymous usage data (command name, score, duration).${R}`);
|
|
866
|
-
console.log(` ${D}No file contents, no PII. To opt out: ${CY}content-grade telemetry off${R}${D} or pass ${CY}--no-telemetry${R}`);
|
|
867
|
-
blank();
|
|
868
|
-
}
|
|
1302
|
+
// Defer first-run telemetry notice so users see value first
|
|
1303
|
+
const _showTelemNotice = _telem.isNew;
|
|
869
1304
|
|
|
870
1305
|
// Smart path detection: first arg is a path if it starts with ., /, ~ or is an existing FS entry
|
|
871
1306
|
function looksLikePath(s) {
|
|
@@ -879,6 +1314,7 @@ switch (cmd) {
|
|
|
879
1314
|
case 'analyze':
|
|
880
1315
|
case 'analyse':
|
|
881
1316
|
case 'check':
|
|
1317
|
+
case 'grade':
|
|
882
1318
|
recordEvent({ event: 'command', command: 'analyze' });
|
|
883
1319
|
cmdAnalyze(args[1]).catch(err => {
|
|
884
1320
|
blank();
|
|
@@ -899,7 +1335,6 @@ switch (cmd) {
|
|
|
899
1335
|
break;
|
|
900
1336
|
|
|
901
1337
|
case 'headline':
|
|
902
|
-
case 'grade':
|
|
903
1338
|
recordEvent({ event: 'command', command: 'headline' });
|
|
904
1339
|
cmdHeadline(args.slice(1).join(' ')).catch(err => {
|
|
905
1340
|
blank();
|
|
@@ -926,6 +1361,27 @@ switch (cmd) {
|
|
|
926
1361
|
});
|
|
927
1362
|
break;
|
|
928
1363
|
|
|
1364
|
+
case 'activate':
|
|
1365
|
+
case 'license':
|
|
1366
|
+
recordEvent({ event: 'command', command: 'activate' });
|
|
1367
|
+
cmdActivate().catch(err => {
|
|
1368
|
+
blank();
|
|
1369
|
+
fail(`Activation error: ${err.message}`);
|
|
1370
|
+
blank();
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
});
|
|
1373
|
+
break;
|
|
1374
|
+
|
|
1375
|
+
case 'batch':
|
|
1376
|
+
recordEvent({ event: 'command', command: 'batch' });
|
|
1377
|
+
cmdBatch(args[1]).catch(err => {
|
|
1378
|
+
blank();
|
|
1379
|
+
fail(`Batch error: ${err.message}`);
|
|
1380
|
+
blank();
|
|
1381
|
+
process.exit(1);
|
|
1382
|
+
});
|
|
1383
|
+
break;
|
|
1384
|
+
|
|
929
1385
|
case 'help':
|
|
930
1386
|
case '--help':
|
|
931
1387
|
case '-h':
|
|
@@ -984,7 +1440,15 @@ switch (cmd) {
|
|
|
984
1440
|
break;
|
|
985
1441
|
|
|
986
1442
|
default:
|
|
987
|
-
if (
|
|
1443
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
1444
|
+
recordEvent({ event: 'command', command: 'analyze_url' });
|
|
1445
|
+
cmdAnalyzeUrl(raw).catch(err => {
|
|
1446
|
+
blank();
|
|
1447
|
+
fail(`Unexpected error: ${err.message}`);
|
|
1448
|
+
blank();
|
|
1449
|
+
process.exit(1);
|
|
1450
|
+
});
|
|
1451
|
+
} else if (looksLikePath(raw)) {
|
|
988
1452
|
// Directory: find best content file inside it
|
|
989
1453
|
let target;
|
|
990
1454
|
try {
|
package/bin/telemetry.js
CHANGED
|
@@ -201,6 +201,40 @@ export function telemetryStatus() {
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// ── Rate limiting ─────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
const FREE_DAILY_LIMIT = 50;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check whether the user is within the free daily limit for a given command type.
|
|
210
|
+
* Type: 'analyze' | 'headline'
|
|
211
|
+
* Returns { ok: boolean, used: number, remaining: number, isPro: boolean }
|
|
212
|
+
*/
|
|
213
|
+
export function checkDailyLimit(type = 'analyze') {
|
|
214
|
+
const cfg = loadConfig();
|
|
215
|
+
if (cfg?.licenseKey) return { ok: true, used: 0, remaining: Infinity, isPro: true };
|
|
216
|
+
|
|
217
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
218
|
+
const used = (cfg?.dailyUsage?.date === today ? cfg.dailyUsage[type] ?? 0 : 0);
|
|
219
|
+
const remaining = FREE_DAILY_LIMIT - used;
|
|
220
|
+
return { ok: remaining > 0, used, remaining: Math.max(0, remaining), isPro: false };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Increment the daily usage counter for a command type after a successful run.
|
|
225
|
+
* Returns the new usage count.
|
|
226
|
+
*/
|
|
227
|
+
export function incrementDailyUsage(type = 'analyze') {
|
|
228
|
+
const cfg = loadConfig() || {};
|
|
229
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
230
|
+
if (!cfg.dailyUsage || cfg.dailyUsage.date !== today) {
|
|
231
|
+
cfg.dailyUsage = { date: today, analyze: 0, headline: 0 };
|
|
232
|
+
}
|
|
233
|
+
cfg.dailyUsage[type] = (cfg.dailyUsage[type] ?? 0) + 1;
|
|
234
|
+
saveConfig(cfg);
|
|
235
|
+
return cfg.dailyUsage[type];
|
|
236
|
+
}
|
|
237
|
+
|
|
204
238
|
// ── Internal ──────────────────────────────────────────────────────────────────
|
|
205
239
|
|
|
206
240
|
function _write(event) {
|
|
@@ -5,7 +5,7 @@ import { getDb } from '../db.js';
|
|
|
5
5
|
import { askClaude } from '../claude.js';
|
|
6
6
|
import { hasActiveSubscriptionLive, hasPurchased } from '../services/stripe.js';
|
|
7
7
|
// ── Usage tracking utilities ──────────────────────────────────────
|
|
8
|
-
const FREE_TIER_LIMIT =
|
|
8
|
+
const FREE_TIER_LIMIT = 50;
|
|
9
9
|
const PRO_TIER_LIMIT = 100;
|
|
10
10
|
const HEADLINE_GRADER_ENDPOINT = 'headline-grader';
|
|
11
11
|
const UPGRADE_URL = 'https://contentgrade.ai/#pricing';
|
package/package.json
CHANGED