content-grade 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTRIBUTING.md CHANGED
@@ -1,33 +1,73 @@
1
1
  # Contributing to Content-Grade
2
2
 
3
+ Thanks for your interest in contributing. Content-Grade is a CLI and web tool for grading content with Claude — bugs, feature ideas, and code contributions are all welcome.
4
+
5
+ ## Quick links
6
+
7
+ - [Report a bug](https://github.com/StanislavBG/Content-Grade/issues/new?template=bug_report.yml)
8
+ - [Request a feature](https://github.com/StanislavBG/Content-Grade/issues/new?template=feature_request.yml)
9
+ - [Ask a question](https://github.com/StanislavBG/Content-Grade/discussions/categories/q-a)
10
+ - [Share what you built](https://github.com/StanislavBG/Content-Grade/discussions/categories/show-and-tell)
11
+ - [Roadmap](ROADMAP.md)
12
+
3
13
  ## Reporting bugs
4
14
 
5
15
  Open an issue using the **Bug report** template. Include:
6
16
  - Your Node.js version (`node --version`)
7
17
  - The exact command you ran
8
- - Output of `npx content-grade init` if it's a setup issue
18
+ - Output of `npx content-grade@latest init` if it's a setup issue
9
19
  - Expected vs. actual behavior
10
20
 
11
- ## Suggesting features
21
+ ## Requesting features
22
+
23
+ Open an issue using **Feature request**, or post in [Discussions → Ideas](https://github.com/StanislavBG/Content-Grade/discussions/categories/ideas). Describe the problem you're trying to solve, not just the solution you have in mind.
24
+
25
+ ## Development setup
26
+
27
+ ```bash
28
+ # Clone and install
29
+ git clone https://github.com/StanislavBG/Content-Grade.git
30
+ cd Content-Grade
31
+ npm install
32
+
33
+ # Run in development
34
+ node bin/content-grade.js demo # smoke test — no API key needed
35
+ node bin/content-grade.js analyze README.md # requires ANTHROPIC_API_KEY
36
+
37
+ # Run all checks (must pass before opening a PR)
38
+ npm test # vitest — all tests green
39
+ npm run typecheck # zero TypeScript errors
40
+ npm run build # vite + tsc — must compile clean
41
+ ```
42
+
43
+ ### Environment variables
44
+
45
+ Copy `.env.example` to `.env` if one exists, or set these:
12
46
 
13
- Open an issue using **Feature request**, or post in [GitHub Discussions → Ideas](https://github.com/StanislavBG/Content-Grade/discussions/categories/ideas). Describe the use case, not just the feature.
47
+ | Variable | Required | Description |
48
+ |----------|----------|-------------|
49
+ | `ANTHROPIC_API_KEY` | For analysis tools | Your Anthropic API key |
50
+ | `PORT` | No (default 3001) | Port for the web dashboard server |
51
+
52
+ The `demo` command and all tests run without an API key — no credentials needed to contribute.
14
53
 
15
54
  ## Submitting a pull request
16
55
 
17
56
  1. Fork the repo, create a branch from `main`
18
- 2. Make changes — see code style notes below
19
- 3. Run the full check before opening a PR:
57
+ 2. Make your change
58
+ 3. Run the full check:
20
59
 
21
60
  ```bash
22
- npm test # all tests must pass
23
- npm run typecheck # zero TypeScript errors
61
+ npm test
62
+ npm run typecheck
63
+ npm run build
24
64
  ```
25
65
 
26
66
  4. Open a PR with a clear description of what changed and why
27
67
 
28
- ## Code style
68
+ The PR template has a checklist — fill it out. Small, focused PRs review faster.
29
69
 
30
- Scripts from `package.json` tell you what tools are in play:
70
+ ## Code style
31
71
 
32
72
  | Script | What it does |
33
73
  |--------|-------------|
@@ -37,11 +77,21 @@ Scripts from `package.json` tell you what tools are in play:
37
77
  | `npm run typecheck` | `tsc --noEmit` — types must be clean |
38
78
  | `npm run build` | vite + tsc server build — must pass before release |
39
79
 
80
+ Rules:
40
81
  - TypeScript throughout — no `any` without a comment explaining why
41
82
  - CLI errors use `fail()` helper, never raw `console.error` + `process.exit`
42
83
  - New Claude prompts must include the scoring calibration note (see existing prompts)
43
84
  - No hardcoded API keys or credentials anywhere
44
85
 
45
- ## Questions
86
+ ## What gets merged
87
+
88
+ - Bug fixes with a test that reproduces the bug
89
+ - Features that have a clear use case described in an issue first
90
+ - Documentation improvements — always welcome
91
+ - Performance improvements with benchmarks
92
+
93
+ If you're unsure whether a PR will be accepted, open an issue or Discussion first.
94
+
95
+ ## Code of conduct
46
96
 
47
- Post in [GitHub Discussions Q&A](https://github.com/StanislavBG/Content-Grade/discussions/categories/q-a).
97
+ This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md). Be kind, be constructive.
package/README.md CHANGED
@@ -1007,6 +1007,20 @@ See [docs/social-proof/built-with-badge.md](docs/social-proof/built-with-badge.m
1007
1007
 
1008
1008
  ---
1009
1009
 
1010
+ ## Community
1011
+
1012
+ [![GitHub Stars](https://img.shields.io/github/stars/StanislavBG/Content-Grade?style=social)](https://github.com/StanislavBG/Content-Grade)
1013
+
1014
+ **1,159 developers have installed Content-Grade — join the community.**
1015
+
1016
+ If Content-Grade saves you time, a ⭐ goes a long way. It helps more developers find the tool and keeps the project active.
1017
+
1018
+ **Early adopter program:** The first 50 seats are still open — early adopters get permanent free Pro tier. [Claim your seat →](https://content-grade.github.io/Content-Grade/#early-adopter)
1019
+
1020
+ **Found a bug? Have a use case?** [Open an issue](https://github.com/StanislavBG/Content-Grade/issues/new/choose) or [start a discussion](https://github.com/StanislavBG/Content-Grade/discussions/new) — every report shapes what gets built next.
1021
+
1022
+ ---
1023
+
1010
1024
  ## Legal
1011
1025
 
1012
1026
  - [Privacy Policy](https://content-grade.github.io/Content-Grade/privacy.html)
@@ -33,6 +33,15 @@ const CONFIG_DIR = resolve(homedir(), '.config', 'content-grade');
33
33
  const CONFIG_FILE = resolve(CONFIG_DIR, 'config.json');
34
34
  const USAGE_FILE = resolve(CONFIG_DIR, 'usage.json');
35
35
 
36
+ // License file — canonical store for activated license keys
37
+ const LICENSE_DIR = resolve(homedir(), '.content-grade');
38
+ const LICENSE_FILE = resolve(LICENSE_DIR, 'license.json');
39
+
40
+ // Valid format: CG-XXXX-XXXX-XXXX-XXXX (16 hex chars in 4 groups of 4)
41
+ function isValidKeyFormat(key) {
42
+ return typeof key === 'string' && /^CG-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/i.test(key.trim());
43
+ }
44
+
36
45
  function loadConfig() {
37
46
  try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; }
38
47
  }
@@ -43,12 +52,21 @@ function saveConfig(data) {
43
52
  }
44
53
 
45
54
  function getLicenseKey() {
46
- return process.env.CONTENT_GRADE_KEY || loadConfig().licenseKey || null;
55
+ if (process.env.CONTENT_GRADE_KEY) return process.env.CONTENT_GRADE_KEY;
56
+ const configKey = loadConfig().licenseKey;
57
+ if (configKey) return configKey;
58
+ // Also check canonical license file
59
+ try {
60
+ const lic = JSON.parse(readFileSync(LICENSE_FILE, 'utf8'));
61
+ return lic.key || null;
62
+ } catch { return null; }
47
63
  }
48
64
 
49
65
  function getLicenseTier() {
50
66
  const cfg = loadConfig();
51
- return cfg.tier || 'free'; // free | starter | pro | team
67
+ const tier = cfg.tier || 'free';
68
+ // Collapse legacy 'starter' tier to 'pro' — Starter was removed
69
+ return tier === 'starter' ? 'pro' : tier;
52
70
  }
53
71
 
54
72
  function isProUser() { return Boolean(getLicenseKey()); }
@@ -71,45 +89,105 @@ function incrementUsage() {
71
89
  return u.count;
72
90
  }
73
91
 
74
- // Usage tiersdaily limits per plan (properly scaled: higher tiers = better $/run)
75
- const TIER_LIMITS = {
76
- free: 5, // 5/day — try it out
77
- starter: 10, // 10/day — $0.030/run
78
- pro: 50, // 50/day — $0.019/run (best value per run)
79
- team: 200, // 200/day — $0.013/run (bulk)
80
- };
81
-
92
+ // Upgrade linksFree Pro Business Team
82
93
  const UPGRADE_LINKS = {
83
- free: 'https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a', // Starter $9/mo
84
- starter: 'https://buy.stripe.com/bJefZjafO2Tz36Z2W48k80b', // Pro $29/mo
85
- pro: 'https://buy.stripe.com/cNiaEZfA8cu9bDv4088k80c', // Team $79/mo
94
+ free: 'https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a', // Pro $9/mo — direct checkout
95
+ pro: 'https://buy.stripe.com/bJefZjafO2Tz36Z2W48k80b', // Business $29/mo
96
+ business: 'https://buy.stripe.com/cNiaEZfA8cu9bDv4088k80c', // Team $79/mo
86
97
  };
87
98
 
88
99
  const TIER_NAMES = {
89
- free: 'Free (5/day)',
90
- starter: 'Starter — $9/mo (10/day)',
91
- pro: 'Pro — $29/mo (50/day)',
92
- team: 'Team — $79/mo (200/day)',
100
+ free: 'Free',
101
+ pro: 'Pro',
102
+ business: 'Business',
103
+ team: 'Team',
104
+ };
105
+
106
+ const TIER_LIMITS = {
107
+ free: 3,
108
+ pro: Infinity,
109
+ business: 100,
110
+ team: 500,
93
111
  };
94
112
 
95
- function getDailyLimit() {
113
+ function getUpgradeMessage() {
96
114
  const tier = getLicenseTier();
97
- return TIER_LIMITS[tier] || TIER_LIMITS.free;
115
+ if (tier === 'free') return `Upgrade to Pro — unlimited analyses for $9/mo → ${UPGRADE_LINKS.free}`;
116
+ if (tier === 'pro') return `Upgrade to Business — 100 analyses/day for $29/mo → ${UPGRADE_LINKS.pro}`;
117
+ if (tier === 'business') return `Upgrade to Team — 500 analyses/day for $79/mo → ${UPGRADE_LINKS.business}`;
118
+ return '';
98
119
  }
99
120
 
100
- function checkDailyLimit() {
101
- const limit = getDailyLimit();
102
- const u = getUsage();
103
- if (u.count >= limit) return { ok: false, count: u.count, limit };
104
- return { ok: true, count: u.count, limit };
121
+ // Show usage-aware upgrade CTA after each free run.
122
+ // count = today's usage AFTER this run (1 = first run, 3 = last free run).
123
+ function showFreeTierCTA(count) {
124
+ const limit = TIER_LIMITS.free; // 3
125
+ const remaining = Math.max(0, limit - count);
126
+
127
+ blank();
128
+ hr();
129
+
130
+ if (remaining === 0) {
131
+ // Last free run used — maximum urgency
132
+ console.log(` ${RD}${B}Daily limit reached${R} ${D}(${count}/${limit} free runs used today)${R}`);
133
+ blank();
134
+ console.log(` That's ${count} analyses today. Pro is ${B}$9/mo${R} — run as many as you need.`);
135
+ console.log(` ${B}Unlimited + batch mode${R} — grade an entire directory in one command.`);
136
+ blank();
137
+ console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
138
+ console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
139
+ blank();
140
+ console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
141
+ } else if (remaining === 1) {
142
+ // 1 run left — build urgency
143
+ console.log(` ${YL}${B}${count}/${limit} free runs used${R} ${D}· 1 remaining${R}`);
144
+ blank();
145
+ console.log(` Last free run of the day. Pro is ${B}$9/mo${R} — unlimited + batch mode.`);
146
+ console.log(` ${MG}${B}→ Upgrade now:${R} ${CY}${UPGRADE_LINKS.free}${R}`);
147
+ blank();
148
+ console.log(` ${D}Or use your last run: ${CY}content-grade analyze ./another-post.md${R}`);
149
+ } else {
150
+ // 1+ runs left — light, value-focused nudge
151
+ console.log(` ${D}Free tier: ${count}/${limit} runs used today · ${remaining} remaining${R}`);
152
+ blank();
153
+ console.log(` ${B}What to do next:${R}`);
154
+ blank();
155
+ console.log(` ${CY}content-grade headline "Your title here"${R} ${D}# grade a headline in ~5s${R}`);
156
+ console.log(` ${CY}content-grade analyze ./another-post.md${R} ${D}# analyze another file${R}`);
157
+ console.log(` ${CY}content-grade analyze https://yoursite.com/post${R} ${D}# audit any live URL${R}`);
158
+ blank();
159
+ console.log(` ${MG}Unlock batch mode:${R} grade a whole directory at once`);
160
+ console.log(` ${D} Have a key? ${CY}content-grade activate${R}`);
161
+ console.log(` Get Pro — $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
162
+ }
105
163
  }
106
164
 
107
- function getUpgradeMessage() {
108
- const tier = getLicenseTier();
109
- const link = UPGRADE_LINKS[tier];
110
- if (!link) return ''; // team tier — no upgrade available
111
- const nextTier = tier === 'free' ? 'Starter ($9/mo, 20/day)' : tier === 'starter' ? 'Pro ($29/mo, 50/day)' : 'Team ($79/mo, 100/day)';
112
- return `Upgrade to ${nextTier} → ${link}`;
165
+ // Block a run before it starts if the free tier is exhausted.
166
+ // Returns true (blocked) and prints a visually distinct upgrade prompt.
167
+ // Returns false if the user may proceed.
168
+ function checkFreeTierLimit() {
169
+ if (isProUser()) return false;
170
+ const usage = getUsage();
171
+ const limit = TIER_LIMITS.free;
172
+ if (usage.count < limit) return false;
173
+
174
+ blank();
175
+ hr();
176
+ console.log(` ${RD}${B}Daily limit reached${R} ${D}(${usage.count}/${limit} free runs used today)${R}`);
177
+ blank();
178
+ console.log(` ${B}Upgrade to Pro — $9/mo${R} to keep going:`);
179
+ blank();
180
+ console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} no daily cap`);
181
+ console.log(` ${GN}✓${R} ${B}Batch mode${R} grade an entire directory at once`);
182
+ console.log(` ${GN}✓${R} ${B}URL analysis${R} audit any live page`);
183
+ blank();
184
+ console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
185
+ console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
186
+ blank();
187
+ console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
188
+ hr();
189
+ blank();
190
+ return true;
113
191
  }
114
192
 
115
193
  let _version = '1.0.0';
@@ -377,20 +455,7 @@ async function cmdAnalyze(filePath) {
377
455
  process.exit(1);
378
456
  }
379
457
 
380
- // Free tier daily limit
381
- const limitCheck = checkDailyLimit();
382
- if (!limitCheck.ok) {
383
- blank();
384
- fail(`Daily limit reached (${limitCheck.count}/${limitCheck.limit} runs used today on ${TIER_NAMES[getLicenseTier()]} plan).`);
385
- blank();
386
- console.log(` ${B}Options:${R}`);
387
- console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
388
- console.log(` ${D}· Upgrade your plan: ${CY}content-grade activate${R}`);
389
- blank();
390
- console.log(` ${MG}${getUpgradeMessage()}${R}`);
391
- blank();
392
- process.exit(1);
393
- }
458
+ if (checkFreeTierLimit()) { process.exit(1); }
394
459
 
395
460
  if (!_jsonMode && !_quietMode) {
396
461
  banner();
@@ -535,8 +600,8 @@ async function cmdAnalyze(filePath) {
535
600
  blank();
536
601
  }
537
602
 
538
- // Track usage
539
- incrementUsage();
603
+ // Track usage — skip entirely for licensed users
604
+ const usageCount = isProUser() ? 0 : incrementUsage();
540
605
 
541
606
  // Save to file if --save flag is set
542
607
  if (_saveMode && result) {
@@ -551,25 +616,13 @@ async function cmdAnalyze(filePath) {
551
616
  }
552
617
  }
553
618
 
554
- // Next step — graduated CTA after user has seen value
555
- blank();
556
- hr();
619
+ // Next step — usage-aware CTA after user has seen value
557
620
  if (isProUser()) {
621
+ blank();
622
+ hr();
558
623
  console.log(` ${D}Pro active · Next: ${CY}content-grade batch ./posts/${R}${D} to analyze a whole directory${R}`);
559
624
  } else {
560
- const usage = getUsage();
561
- const remaining = Math.max(0, FREE_DAILY_LIMIT - usage.count);
562
- console.log(` ${B}What to do next:${R}`);
563
- blank();
564
- console.log(` ${CY}content-grade headline "Your title here"${R} ${D}# grade a headline in ~5s${R}`);
565
- console.log(` ${CY}content-grade analyze ./another-post.md${R} ${D}# analyze another file${R}`);
566
- console.log(` ${CY}content-grade analyze https://yoursite.com/post${R} ${D}# audit any live URL${R}`);
567
- blank();
568
- if (remaining <= 10) {
569
- console.log(` ${YL}${remaining} runs left today (${TIER_NAMES[getLicenseTier()]}).${R} ${MG}${getUpgradeMessage()}${R}`);
570
- } else {
571
- console.log(` ${D}${remaining} runs left today. ${getUpgradeMessage()}${R}`);
572
- }
625
+ showFreeTierCTA(usageCount);
573
626
  }
574
627
 
575
628
  // CI exit code — shown after full output so user sees the score before exit
@@ -642,20 +695,7 @@ async function cmdHeadline(text) {
642
695
  process.exit(1);
643
696
  }
644
697
 
645
- // Free tier daily limit
646
- const limitCheck = checkDailyLimit();
647
- if (!limitCheck.ok) {
648
- blank();
649
- fail(`Daily limit reached (${limitCheck.count}/${limitCheck.limit} runs used today on ${TIER_NAMES[getLicenseTier()]} plan).`);
650
- blank();
651
- console.log(` ${B}Options:${R}`);
652
- console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
653
- console.log(` ${D}· Upgrade your plan: ${CY}content-grade activate${R}`);
654
- blank();
655
- console.log(` ${MG}${getUpgradeMessage()}${R}`);
656
- blank();
657
- process.exit(1);
658
- }
698
+ if (checkFreeTierLimit()) { process.exit(1); }
659
699
 
660
700
  if (!_jsonMode && !_quietMode) {
661
701
  banner();
@@ -742,8 +782,8 @@ async function cmdHeadline(text) {
742
782
  console.log(` ${D}Compare two headlines: ${CY}content-grade start${R} → HeadlineGrader compare${R}`);
743
783
  blank();
744
784
  if (!isProUser()) {
745
- console.log(` ${MG}${getUpgradeMessage()}${R}`);
746
- blank();
785
+ const usageCount = incrementUsage();
786
+ showFreeTierCTA(usageCount);
747
787
  }
748
788
  }
749
789
 
@@ -823,7 +863,7 @@ async function cmdInit() {
823
863
  console.log(` ${CY}content-grade start${R}`);
824
864
  blank();
825
865
  }
826
- console.log(` ${D}Pro tier: 100 analyses/day + batch mode — ${CY}content-grade.onrender.com${R}`);
866
+ console.log(` ${D}Pro tier: unlimited analyses/day + batch mode — ${CY}content-grade.onrender.com${R}`);
827
867
  blank();
828
868
  }
829
869
 
@@ -843,14 +883,14 @@ async function cmdActivate() {
843
883
  blank();
844
884
  console.log(` ${B}Pro features:${R}`);
845
885
  console.log(` ${D} content-grade batch ./posts/ ${R}${D}# analyze all files in a directory${R}`);
846
- console.log(` ${D} 100 checks/day (vs 50 free)${R}`);
886
+ console.log(` ${D} Unlimited analyses/day (vs 3 free)${R}`);
847
887
  blank();
848
888
  return;
849
889
  }
850
890
 
851
891
  console.log(` ${D}Pro tier unlocks:${R}`);
852
892
  console.log(` ${D} · ${CY}content-grade batch <dir>${R}${D} — analyze entire directories${R}`);
853
- console.log(` ${D} · 100 checks/day (vs 50 free)${R}`);
893
+ console.log(` ${D} · Unlimited analyses/day (vs 3 free)${R}`);
854
894
  blank();
855
895
  console.log(` ${D}Get a license: ${CY}content-grade.onrender.com${R}${D} → Pricing${R}`);
856
896
  blank();
@@ -899,6 +939,14 @@ async function cmdActivate() {
899
939
  process.exit(1);
900
940
  }
901
941
 
942
+ // Validate key format: CG-XXXX-XXXX-XXXX-XXXX
943
+ if (!isValidKeyFormat(key)) {
944
+ blank();
945
+ fail(`Invalid key format. Expected: CG-XXXX-XXXX-XXXX-XXXX (e.g. CG-A1B2-C3D4-E5F6-A7B8)`);
946
+ blank();
947
+ process.exit(1);
948
+ }
949
+
902
950
  // Validate key against server before storing
903
951
  const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://content-grade.onrender.com';
904
952
  blank();
@@ -945,6 +993,14 @@ async function cmdActivate() {
945
993
  config.activatedAt = new Date().toISOString();
946
994
  saveConfig(config);
947
995
 
996
+ // Also write canonical license file to ~/.content-grade/license.json
997
+ mkdirSync(LICENSE_DIR, { recursive: true });
998
+ writeFileSync(LICENSE_FILE, JSON.stringify({
999
+ key,
1000
+ tier: activatedTier,
1001
+ activatedAt: config.activatedAt,
1002
+ }, null, 2), 'utf8');
1003
+
948
1004
  const tierLimit = TIER_LIMITS[activatedTier] || TIER_LIMITS.free;
949
1005
  blank();
950
1006
  ok(`${(TIER_NAMES[activatedTier] || activatedTier).split(' —')[0]} activated! Your daily limit is now ${tierLimit} analyses.`);
@@ -1072,7 +1128,7 @@ async function cmdBatch(dirPath) {
1072
1128
  'claude-haiku-4-5-20251001'
1073
1129
  );
1074
1130
  const r = parseJSON(raw);
1075
- incrementUsage();
1131
+ if (!isProUser()) incrementUsage();
1076
1132
  const sc = r.total_score;
1077
1133
  const scoreColor = sc >= 70 ? GN : sc >= 45 ? YL : RD;
1078
1134
  process.stdout.write(`\r ${scoreColor}${String(sc).padStart(3)}/100${R} ${gradeLetter(sc)} ${rel}${' '.repeat(10)}\n`);
@@ -1444,14 +1500,12 @@ function cmdHelp() {
1444
1500
  console.log(` · Node.js 18+`);
1445
1501
  blank();
1446
1502
 
1447
- console.log(` ${B}PRO TIER${R} ${MG}$9/mo${R}`);
1503
+ console.log(` ${B}PRICING${R}`);
1448
1504
  blank();
1449
- console.log(` · 100 analyses/day (vs 50 free)`);
1450
- console.log(` · Competitor headline A/B comparison`);
1451
- console.log(` · Landing page URL audit`);
1452
- console.log(` · Ad copy scoring (Google, Meta, LinkedIn)`);
1453
- console.log(` · Email subject line optimizer`);
1454
- console.log(` · Audience archetype decoder`);
1505
+ console.log(` Free 3/day $0`);
1506
+ console.log(` Pro Unlimited $9/mo`);
1507
+ console.log(` Business 100/day $29/mo`);
1508
+ console.log(` Team 500/day $79/mo`);
1455
1509
  blank();
1456
1510
  console.log(` Purchase at: ${CY}content-grade.onrender.com${R}`);
1457
1511
  console.log(` Then unlock: ${CY}content-grade activate <your-license-key>${R}`);
@@ -1622,10 +1676,9 @@ async function cmdDemo() {
1622
1676
  console.log(` ${CY}npx content-grade headline "Your title here"${R} ${D}# grade a single headline${R}`);
1623
1677
  blank();
1624
1678
 
1625
- // Show telemetry notice after user has seen value (first run only)
1679
+ // Show telemetry notice on first run (opt-out model)
1626
1680
  if (_showTelemNotice) {
1627
- console.log(` ${D}Help improve ContentGradeenable anonymous usage analytics: ${CY}content-grade telemetry on${R}`);
1628
- console.log(` ${D}No PII, no file contents. View what's collected: ${CY}content-grade telemetry${R}`);
1681
+ console.log(` ${D}Anonymous usage analytics enabled no PII, no file contents. Opt out: ${CY}content-grade telemetry off${R}`);
1629
1682
  blank();
1630
1683
  }
1631
1684
 
@@ -1922,7 +1975,7 @@ function analysisToMarkdown(result, sourceName, analyzedAt) {
1922
1975
  }
1923
1976
 
1924
1977
  lines.push(`---`);
1925
- lines.push(`Generated by [ContentGrade](https://buy.stripe.com/5kQeVfew48dT7nf2W48k801) v${_version}`);
1978
+ lines.push(`Generated by [ContentGrade](https://content-grade.onrender.com) v${_version}`);
1926
1979
  return lines.join('\n');
1927
1980
  }
1928
1981
 
@@ -2071,7 +2124,7 @@ if (_rawArgs.includes('--help') || _rawArgs.includes('-h')) {
2071
2124
  }
2072
2125
 
2073
2126
  if (_demoMode) {
2074
- recordEvent({ event: 'command', command: 'demo' });
2127
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'demo' });
2075
2128
  cmdDemo().catch(err => {
2076
2129
  blank();
2077
2130
  fail(`Demo error: ${err.message}`);
@@ -2083,7 +2136,7 @@ if (_demoMode) {
2083
2136
  case 'analyse':
2084
2137
  case 'check':
2085
2138
  case 'grade':
2086
- recordEvent({ event: 'command', command: 'analyze' });
2139
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze' });
2087
2140
  cmdAnalyze(args[1]).catch(err => {
2088
2141
  blank();
2089
2142
  fail(`Unexpected error: ${err.message}`);
@@ -2093,7 +2146,7 @@ if (_demoMode) {
2093
2146
  break;
2094
2147
 
2095
2148
  case 'demo':
2096
- recordEvent({ event: 'command', command: 'demo' });
2149
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'demo' });
2097
2150
  cmdDemo().catch(err => {
2098
2151
  blank();
2099
2152
  fail(`Demo error: ${err.message}`);
@@ -2103,7 +2156,7 @@ if (_demoMode) {
2103
2156
  break;
2104
2157
 
2105
2158
  case 'headline':
2106
- recordEvent({ event: 'command', command: 'headline' });
2159
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'headline' });
2107
2160
  cmdHeadline(args.slice(1).join(' ')).catch(err => {
2108
2161
  blank();
2109
2162
  fail(`Unexpected error: ${err.message}`);
@@ -2114,13 +2167,13 @@ if (_demoMode) {
2114
2167
 
2115
2168
  case 'start':
2116
2169
  case 'serve':
2117
- recordEvent({ event: 'command', command: 'start' });
2170
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'start' });
2118
2171
  cmdStart();
2119
2172
  break;
2120
2173
 
2121
2174
  case 'init':
2122
2175
  case 'setup':
2123
- recordEvent({ event: 'command', command: 'init' });
2176
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'init' });
2124
2177
  cmdInit().catch(err => {
2125
2178
  blank();
2126
2179
  fail(`Setup error: ${err.message}`);
@@ -2131,7 +2184,7 @@ if (_demoMode) {
2131
2184
 
2132
2185
  case 'activate':
2133
2186
  case 'license':
2134
- recordEvent({ event: 'command', command: 'activate' });
2187
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'activate' });
2135
2188
  cmdActivate().catch(err => {
2136
2189
  blank();
2137
2190
  fail(`Activation error: ${err.message}`);
@@ -2142,7 +2195,7 @@ if (_demoMode) {
2142
2195
 
2143
2196
  case 'seo-audit':
2144
2197
  case 'seoaudit':
2145
- recordEvent({ event: 'command', command: 'seo-audit' });
2198
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'seo-audit' });
2146
2199
  if (!args[1]) {
2147
2200
  blank();
2148
2201
  fail(`No URL specified.`);
@@ -2161,7 +2214,7 @@ if (_demoMode) {
2161
2214
  break;
2162
2215
 
2163
2216
  case 'batch':
2164
- recordEvent({ event: 'command', command: 'batch' });
2217
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'batch' });
2165
2218
  cmdBatch(args[1]).catch(err => {
2166
2219
  blank();
2167
2220
  fail(`Batch error: ${err.message}`);
@@ -2179,7 +2232,7 @@ if (_demoMode) {
2179
2232
  case 'check-updates':
2180
2233
  case 'update':
2181
2234
  case 'upgrade-check':
2182
- recordEvent({ event: 'command', command: 'check-updates' });
2235
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'check-updates' });
2183
2236
  cmdCheckUpdates().catch(err => {
2184
2237
  blank();
2185
2238
  fail(`Update check error: ${err.message}`);
@@ -2234,13 +2287,13 @@ if (_demoMode) {
2234
2287
 
2235
2288
  case undefined:
2236
2289
  // No command — show instant static demo (no Claude required, zero wait)
2237
- recordEvent({ event: 'command', command: 'quick_demo' });
2290
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'quick_demo' });
2238
2291
  cmdQuickDemo();
2239
2292
  break;
2240
2293
 
2241
2294
  default:
2242
2295
  if (/^https?:\/\//i.test(raw)) {
2243
- recordEvent({ event: 'command', command: 'analyze_url' });
2296
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze_url' });
2244
2297
  cmdAnalyzeUrl(raw).catch(err => {
2245
2298
  blank();
2246
2299
  fail(`Unexpected error: ${err.message}`);
@@ -2274,7 +2327,7 @@ if (_demoMode) {
2274
2327
  } catch {
2275
2328
  target = raw; // let cmdAnalyze handle the "not found" error
2276
2329
  }
2277
- recordEvent({ event: 'command', command: 'analyze' });
2330
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze' });
2278
2331
  cmdAnalyze(target).catch(err => {
2279
2332
  blank();
2280
2333
  fail(`Unexpected error: ${err.message}`);
@@ -2287,7 +2340,7 @@ if (_demoMode) {
2287
2340
  // npx content-grade Why most startups fail
2288
2341
  const allText = args.join(' ').trim();
2289
2342
  if (allText && allText.length >= 5 && !allText.startsWith('-')) {
2290
- recordEvent({ event: 'command', command: 'headline_smart' });
2343
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'headline_smart' });
2291
2344
  cmdHeadline(allText).catch(err => {
2292
2345
  blank();
2293
2346
  fail(`Unexpected error: ${err.message}`);