content-grade 1.0.2 → 1.0.4
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 +26 -9
- package/bin/content-grade.js +389 -33
- package/bin/telemetry.js +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,20 +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
|
-
npx content-grade
|
|
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
|
|
26
28
|
|
|
27
|
-
# 5. Grade a
|
|
29
|
+
# 5. Grade a headline
|
|
28
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
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
**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.
|
|
32
39
|
|
|
33
40
|
---
|
|
34
41
|
|
|
@@ -90,15 +97,25 @@ npx content-grade headline "Why Most Startups Fail at Month 18"
|
|
|
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 |
|
package/bin/content-grade.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
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';
|
|
21
22
|
import { get as httpsGet } from 'https';
|
|
@@ -26,6 +27,54 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
26
27
|
const root = resolve(__dirname, '..');
|
|
27
28
|
const execFileAsync = promisify(execFile);
|
|
28
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
|
+
|
|
29
78
|
let _version = '1.0.0';
|
|
30
79
|
try { _version = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')).version ?? '1.0.0'; } catch {}
|
|
31
80
|
|
|
@@ -101,11 +150,27 @@ async function askClaude(prompt, systemPrompt, model = 'claude-haiku-4-5-2025100
|
|
|
101
150
|
if (systemPrompt) args.push('--system-prompt', systemPrompt);
|
|
102
151
|
args.push(prompt);
|
|
103
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();
|
|
104
162
|
const { stdout } = await execFileAsync(claudePath, args, {
|
|
105
163
|
timeout: 120000,
|
|
106
164
|
maxBuffer: 10 * 1024 * 1024,
|
|
107
165
|
});
|
|
108
|
-
|
|
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;
|
|
109
174
|
}
|
|
110
175
|
|
|
111
176
|
function parseJSON(raw) {
|
|
@@ -245,6 +310,19 @@ async function cmdAnalyze(filePath) {
|
|
|
245
310
|
process.exit(2);
|
|
246
311
|
}
|
|
247
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();
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
248
326
|
if (!_jsonMode && !_quietMode) {
|
|
249
327
|
banner();
|
|
250
328
|
console.log(` ${B}Analyzing:${R} ${CY}${basename(absPath)}${R}`);
|
|
@@ -380,9 +458,22 @@ async function cmdAnalyze(filePath) {
|
|
|
380
458
|
blank();
|
|
381
459
|
}
|
|
382
460
|
|
|
383
|
-
//
|
|
461
|
+
// Track usage
|
|
462
|
+
incrementUsage();
|
|
463
|
+
|
|
464
|
+
// Upsell — show Pro path after user has seen value
|
|
384
465
|
blank();
|
|
385
|
-
|
|
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
|
+
}
|
|
386
477
|
blank();
|
|
387
478
|
}
|
|
388
479
|
|
|
@@ -422,6 +513,19 @@ async function cmdHeadline(text) {
|
|
|
422
513
|
console.log(` ${D}content-grade headline "How We Grew From 0 to 10k Users Without Ads"${R}`);
|
|
423
514
|
console.log(` ${D}content-grade headline "Stop Doing This One Thing in Your Email Subject Lines"${R}`);
|
|
424
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();
|
|
425
529
|
process.exit(1);
|
|
426
530
|
}
|
|
427
531
|
|
|
@@ -581,6 +685,207 @@ async function cmdInit() {
|
|
|
581
685
|
blank();
|
|
582
686
|
}
|
|
583
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}`);
|
|
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');
|
|
887
|
+
}
|
|
888
|
+
|
|
584
889
|
// ── Start command ─────────────────────────────────────────────────────────────
|
|
585
890
|
|
|
586
891
|
function checkBuild() {
|
|
@@ -712,28 +1017,36 @@ function cmdHelp() {
|
|
|
712
1017
|
|
|
713
1018
|
console.log(` ${B}COMMANDS${R}`);
|
|
714
1019
|
blank();
|
|
715
|
-
console.log(` ${CY}demo${R}
|
|
1020
|
+
console.log(` ${CY}demo${R} Run on sample content — instant result, no file needed`);
|
|
1021
|
+
blank();
|
|
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}`);
|
|
1027
|
+
blank();
|
|
1028
|
+
console.log(` ${CY}batch <directory>${R} ${MG}[Pro]${R} Analyze all .md/.txt files in a directory`);
|
|
716
1029
|
blank();
|
|
717
|
-
console.log(` ${CY}
|
|
718
|
-
console.log(` ${CY}check <file>${R} Same as analyze`);
|
|
719
|
-
console.log(` ${D} Works on .md, .txt, or any text file${R}`);
|
|
720
|
-
console.log(` ${D} Zero config — just needs Claude CLI${R}`);
|
|
1030
|
+
console.log(` ${CY}headline "<text>"${R} Grade a single headline (4 copywriting frameworks)`);
|
|
721
1031
|
blank();
|
|
722
|
-
console.log(` ${CY}
|
|
723
|
-
console.log(` ${D} Scores on 4 copywriting frameworks${R}`);
|
|
1032
|
+
console.log(` ${CY}activate${R} Enter license key to unlock Pro features`);
|
|
724
1033
|
blank();
|
|
725
|
-
console.log(` ${CY}start${R}
|
|
726
|
-
console.log(` ${D}
|
|
1034
|
+
console.log(` ${CY}start${R} Launch the full web dashboard`);
|
|
1035
|
+
console.log(` ${D} 6 tools: headlines, pages, ads, threads, emails, audiences${R}`);
|
|
727
1036
|
blank();
|
|
728
|
-
console.log(` ${CY}init${R}
|
|
1037
|
+
console.log(` ${CY}init${R} First-run setup and diagnostics`);
|
|
729
1038
|
blank();
|
|
730
|
-
console.log(` ${CY}telemetry [on|off]${R}
|
|
1039
|
+
console.log(` ${CY}telemetry [on|off]${R} View or toggle anonymous usage tracking`);
|
|
731
1040
|
blank();
|
|
732
|
-
console.log(` ${CY}help${R}
|
|
1041
|
+
console.log(` ${CY}help${R} Show this help`);
|
|
733
1042
|
blank();
|
|
734
1043
|
console.log(` ${B}FLAGS${R}`);
|
|
735
1044
|
blank();
|
|
736
|
-
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`);
|
|
737
1050
|
blank();
|
|
738
1051
|
|
|
739
1052
|
console.log(` ${B}EXAMPLES${R}`);
|
|
@@ -741,9 +1054,21 @@ function cmdHelp() {
|
|
|
741
1054
|
console.log(` ${D}# Analyze a blog post${R}`);
|
|
742
1055
|
console.log(` ${CY}content-grade analyze ./my-post.md${R}`);
|
|
743
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();
|
|
744
1066
|
console.log(` ${D}# Grade a headline${R}`);
|
|
745
1067
|
console.log(` ${CY}content-grade headline "How I 10x'd My Conversion Rate in 30 Days"${R}`);
|
|
746
1068
|
blank();
|
|
1069
|
+
console.log(` ${D}# Batch analyze a content directory (Pro)${R}`);
|
|
1070
|
+
console.log(` ${CY}content-grade batch ./posts${R}`);
|
|
1071
|
+
blank();
|
|
747
1072
|
console.log(` ${D}# Launch full dashboard${R}`);
|
|
748
1073
|
console.log(` ${CY}content-grade start${R}`);
|
|
749
1074
|
blank();
|
|
@@ -769,19 +1094,19 @@ function cmdHelp() {
|
|
|
769
1094
|
|
|
770
1095
|
// ── Demo command ──────────────────────────────────────────────────────────────
|
|
771
1096
|
|
|
772
|
-
const DEMO_CONTENT = `#
|
|
1097
|
+
const DEMO_CONTENT = `# How to Write Better Blog Posts
|
|
773
1098
|
|
|
774
|
-
|
|
1099
|
+
Writing good blog posts is important for any content marketer. Many people struggle with this. In this post I will share some tips that can help you improve.
|
|
775
1100
|
|
|
776
|
-
First,
|
|
1101
|
+
First, you need to have a good headline. The headline is the first thing people see. A good headline will make people want to read more. Try to make it interesting.
|
|
777
1102
|
|
|
778
|
-
Second,
|
|
1103
|
+
Second, write good content. Your content should be useful and informative. People should learn something from reading your post. Make sure to cover the topic well.
|
|
779
1104
|
|
|
780
|
-
Third,
|
|
1105
|
+
Third, use a good structure. Break your content into sections. Use headings and bullet points. This makes it easier to read. People can scan and find what they need.
|
|
781
1106
|
|
|
782
|
-
|
|
1107
|
+
Finally, have a good conclusion. Summarize what you talked about. Tell people what to do next. A call to action is important.
|
|
783
1108
|
|
|
784
|
-
In conclusion, these
|
|
1109
|
+
In conclusion, following these tips will help you write better blog posts. Good content is important for success online. Start applying these tips today.
|
|
785
1110
|
`;
|
|
786
1111
|
|
|
787
1112
|
async function cmdDemo() {
|
|
@@ -800,17 +1125,26 @@ async function cmdDemo() {
|
|
|
800
1125
|
const tmpFile = `/tmp/content-grade-demo-${process.pid}-${Date.now()}.md`;
|
|
801
1126
|
|
|
802
1127
|
banner();
|
|
803
|
-
console.log(` ${B}Demo Mode${R} ${D}—
|
|
804
|
-
blank();
|
|
805
|
-
console.log(` ${D}This is what ContentGrade does. Try it on your own content:${R}`);
|
|
806
|
-
console.log(` ${CY} content-grade analyze ./your-post.md${R}`);
|
|
1128
|
+
console.log(` ${B}Demo Mode${R} ${D}— see ContentGrade in action${R}`);
|
|
807
1129
|
blank();
|
|
808
|
-
console.log(` ${D}Analyzing sample blog post
|
|
1130
|
+
console.log(` ${D}Analyzing a sample blog post with generic writing.${R}`);
|
|
1131
|
+
console.log(` ${D}Generic content scores low — that's the point. Watch what ContentGrade finds.${R}`);
|
|
809
1132
|
blank();
|
|
810
1133
|
|
|
811
1134
|
writeFileSync(tmpFile, DEMO_CONTENT, 'utf8');
|
|
812
1135
|
try {
|
|
813
1136
|
await cmdAnalyze(tmpFile);
|
|
1137
|
+
|
|
1138
|
+
// Post-demo CTA — shown AFTER user has seen the analysis output
|
|
1139
|
+
blank();
|
|
1140
|
+
hr();
|
|
1141
|
+
console.log(` ${B}${CY}Now try it on your own content:${R}`);
|
|
1142
|
+
blank();
|
|
1143
|
+
console.log(` ${CY}content-grade analyze ./your-post.md${R} ${D}# any .md or .txt file${R}`);
|
|
1144
|
+
console.log(` ${CY}content-grade https://your-blog.com/post${R} ${D}# analyze any URL${R}`);
|
|
1145
|
+
console.log(` ${CY}content-grade headline "Your headline here"${R} ${D}# grade a headline${R}`);
|
|
1146
|
+
blank();
|
|
1147
|
+
|
|
814
1148
|
// Show telemetry notice after user has seen value (first run only)
|
|
815
1149
|
if (_showTelemNotice) {
|
|
816
1150
|
console.log(` ${D}Anonymous usage data is collected by default. Opt out: ${CY}content-grade telemetry off${R}`);
|
|
@@ -964,10 +1298,11 @@ function findBestContentFile(dirPath) {
|
|
|
964
1298
|
|
|
965
1299
|
// ── Router ────────────────────────────────────────────────────────────────────
|
|
966
1300
|
|
|
967
|
-
const _rawArgs
|
|
968
|
-
const _jsonMode
|
|
969
|
-
const _quietMode
|
|
970
|
-
const
|
|
1301
|
+
const _rawArgs = process.argv.slice(2);
|
|
1302
|
+
const _jsonMode = _rawArgs.includes('--json');
|
|
1303
|
+
const _quietMode = _rawArgs.includes('--quiet');
|
|
1304
|
+
const _verboseMode = _rawArgs.includes('--verbose') || _rawArgs.includes('-v') && !_rawArgs.includes('--version');
|
|
1305
|
+
const args = _rawArgs.filter(a => !['--no-telemetry', '--json', '--quiet', '--verbose'].includes(a));
|
|
971
1306
|
const raw = args[0];
|
|
972
1307
|
const cmd = raw?.toLowerCase();
|
|
973
1308
|
|
|
@@ -988,6 +1323,7 @@ switch (cmd) {
|
|
|
988
1323
|
case 'analyze':
|
|
989
1324
|
case 'analyse':
|
|
990
1325
|
case 'check':
|
|
1326
|
+
case 'grade':
|
|
991
1327
|
recordEvent({ event: 'command', command: 'analyze' });
|
|
992
1328
|
cmdAnalyze(args[1]).catch(err => {
|
|
993
1329
|
blank();
|
|
@@ -1008,7 +1344,6 @@ switch (cmd) {
|
|
|
1008
1344
|
break;
|
|
1009
1345
|
|
|
1010
1346
|
case 'headline':
|
|
1011
|
-
case 'grade':
|
|
1012
1347
|
recordEvent({ event: 'command', command: 'headline' });
|
|
1013
1348
|
cmdHeadline(args.slice(1).join(' ')).catch(err => {
|
|
1014
1349
|
blank();
|
|
@@ -1035,6 +1370,27 @@ switch (cmd) {
|
|
|
1035
1370
|
});
|
|
1036
1371
|
break;
|
|
1037
1372
|
|
|
1373
|
+
case 'activate':
|
|
1374
|
+
case 'license':
|
|
1375
|
+
recordEvent({ event: 'command', command: 'activate' });
|
|
1376
|
+
cmdActivate().catch(err => {
|
|
1377
|
+
blank();
|
|
1378
|
+
fail(`Activation error: ${err.message}`);
|
|
1379
|
+
blank();
|
|
1380
|
+
process.exit(1);
|
|
1381
|
+
});
|
|
1382
|
+
break;
|
|
1383
|
+
|
|
1384
|
+
case 'batch':
|
|
1385
|
+
recordEvent({ event: 'command', command: 'batch' });
|
|
1386
|
+
cmdBatch(args[1]).catch(err => {
|
|
1387
|
+
blank();
|
|
1388
|
+
fail(`Batch error: ${err.message}`);
|
|
1389
|
+
blank();
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
});
|
|
1392
|
+
break;
|
|
1393
|
+
|
|
1038
1394
|
case 'help':
|
|
1039
1395
|
case '--help':
|
|
1040
1396
|
case '-h':
|
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) {
|
package/package.json
CHANGED