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 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 an instant demo — no file needed
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. Analyze a live URL directly
25
- npx content-grade https://yoursite.com/blog/post
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 single headline
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. That's it — no API keys, no accounts, no data leaves your machine.
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
- | `analyse <file>` | Alias for `analyze` (British spelling) |
102
+ | `batch <dir>` | **[Pro]** Analyze all .md/.txt files in a directory |
95
103
  | `headline "<text>"` | Grade a headline on 4 copywriting frameworks |
96
- | `grade "<text>"` | Alias for `headline` |
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 |
@@ -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
- return stdout.trim();
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
- // ── Subtle upsell
461
+ // Track usage
462
+ incrementUsage();
463
+
464
+ // Upsell — show Pro path after user has seen value
384
465
  blank();
385
- console.log(` ${D}Unlock team features at ${CY}contentgrade.dev${R}`);
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} Run on sample content — instant result, no file needed`);
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}analyze <file>${R} Analyze content from a file`);
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}headline "<text>"${R} Grade a single headline`);
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} Launch the full web dashboard`);
726
- console.log(` ${D} 6 tools: headlines, pages, ads, threads...${R}`);
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} First-run setup and diagnostics`);
1037
+ console.log(` ${CY}init${R} First-run setup and diagnostics`);
729
1038
  blank();
730
- console.log(` ${CY}telemetry [on|off]${R} View or toggle anonymous usage tracking`);
1039
+ console.log(` ${CY}telemetry [on|off]${R} View or toggle anonymous usage tracking`);
731
1040
  blank();
732
- console.log(` ${CY}help${R} Show this help`);
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}--no-telemetry${R} Skip usage tracking for this invocation`);
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 = `# Why Your Morning Routine Is Secretly Destroying Your Productivity
1097
+ const DEMO_CONTENT = `# How to Write Better Blog Posts
773
1098
 
774
- Productivity is important. Many people struggle with it. In this post we will discuss some tips.
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, wake up early. Successful people wake up early. This gives you more time. Studies show that early risers are more productive.
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, make a to-do list. Write everything down. Then do the tasks one by one. This is a simple but effective strategy.
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, avoid distractions. Put your phone away. Social media is bad for focus. You should minimize interruptions.
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
- Fourth, take breaks. Working too hard is not good. Make sure to rest sometimes. The Pomodoro technique can help with this.
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 productivity tips are very helpful. Try them today and you will see a big difference in your output. Productivity is the key to success in today's world.
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}— running analysis on sample content${R}`);
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...${R}`);
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 = process.argv.slice(2);
968
- const _jsonMode = _rawArgs.includes('--json');
969
- const _quietMode = _rawArgs.includes('--quiet');
970
- const args = _rawArgs.filter(a => a !== '--no-telemetry' && a !== '--json' && a !== '--quiet');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-grade",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "AI-powered content analysis CLI. Score any blog post, landing page, or ad copy in under 30 seconds — runs on Claude CLI, no API key needed.",
5
5
  "type": "module",
6
6
  "bin": {