content-grade 1.0.2 → 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 +26 -9
- package/bin/content-grade.js +368 -21
- 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}`);
|
|
716
1027
|
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}`);
|
|
1028
|
+
console.log(` ${CY}batch <directory>${R} ${MG}[Pro]${R} Analyze all .md/.txt files in a directory`);
|
|
721
1029
|
blank();
|
|
722
|
-
console.log(` ${CY}headline "<text>"${R}
|
|
723
|
-
console.log(` ${D} Scores on 4 copywriting frameworks${R}`);
|
|
1030
|
+
console.log(` ${CY}headline "<text>"${R} Grade a single headline (4 copywriting frameworks)`);
|
|
724
1031
|
blank();
|
|
725
|
-
console.log(` ${CY}
|
|
726
|
-
console.log(` ${D} 6 tools: headlines, pages, ads, threads...${R}`);
|
|
1032
|
+
console.log(` ${CY}activate${R} Enter license key to unlock Pro features`);
|
|
727
1033
|
blank();
|
|
728
|
-
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}`);
|
|
729
1036
|
blank();
|
|
730
|
-
console.log(` ${CY}
|
|
1037
|
+
console.log(` ${CY}init${R} First-run setup and diagnostics`);
|
|
731
1038
|
blank();
|
|
732
|
-
console.log(` ${CY}
|
|
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`);
|
|
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();
|
|
@@ -964,10 +1289,11 @@ function findBestContentFile(dirPath) {
|
|
|
964
1289
|
|
|
965
1290
|
// ── Router ────────────────────────────────────────────────────────────────────
|
|
966
1291
|
|
|
967
|
-
const _rawArgs
|
|
968
|
-
const _jsonMode
|
|
969
|
-
const _quietMode
|
|
970
|
-
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));
|
|
971
1297
|
const raw = args[0];
|
|
972
1298
|
const cmd = raw?.toLowerCase();
|
|
973
1299
|
|
|
@@ -988,6 +1314,7 @@ switch (cmd) {
|
|
|
988
1314
|
case 'analyze':
|
|
989
1315
|
case 'analyse':
|
|
990
1316
|
case 'check':
|
|
1317
|
+
case 'grade':
|
|
991
1318
|
recordEvent({ event: 'command', command: 'analyze' });
|
|
992
1319
|
cmdAnalyze(args[1]).catch(err => {
|
|
993
1320
|
blank();
|
|
@@ -1008,7 +1335,6 @@ switch (cmd) {
|
|
|
1008
1335
|
break;
|
|
1009
1336
|
|
|
1010
1337
|
case 'headline':
|
|
1011
|
-
case 'grade':
|
|
1012
1338
|
recordEvent({ event: 'command', command: 'headline' });
|
|
1013
1339
|
cmdHeadline(args.slice(1).join(' ')).catch(err => {
|
|
1014
1340
|
blank();
|
|
@@ -1035,6 +1361,27 @@ switch (cmd) {
|
|
|
1035
1361
|
});
|
|
1036
1362
|
break;
|
|
1037
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
|
+
|
|
1038
1385
|
case 'help':
|
|
1039
1386
|
case '--help':
|
|
1040
1387
|
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