content-grade 1.0.19 → 1.0.21

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,7 +52,14 @@ 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() {
@@ -75,9 +91,9 @@ function incrementUsage() {
75
91
 
76
92
  // Upgrade links — Free → Pro → Business → Team
77
93
  const UPGRADE_LINKS = {
78
- free: 'https://content-grade.onrender.com/#pricing', // Pro $9/mo
79
- pro: 'https://content-grade.onrender.com/#pricing', // Business $29/mo
80
- business: 'https://content-grade.onrender.com/#pricing', // 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
81
97
  };
82
98
 
83
99
  const TIER_NAMES = {
@@ -89,16 +105,16 @@ const TIER_NAMES = {
89
105
 
90
106
  const TIER_LIMITS = {
91
107
  free: 3,
92
- pro: 20,
108
+ pro: Infinity,
93
109
  business: 100,
94
110
  team: 500,
95
111
  };
96
112
 
97
113
  function getUpgradeMessage() {
98
114
  const tier = getLicenseTier();
99
- if (tier === 'free') return `Upgrade to Pro (20/day) → ${UPGRADE_LINKS.free}`;
100
- if (tier === 'pro') return `Upgrade to Business (100/day) → ${UPGRADE_LINKS.pro}`;
101
- if (tier === 'business') return `Upgrade to Team (500/day) → ${UPGRADE_LINKS.business}`;
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}`;
102
118
  return '';
103
119
  }
104
120
 
@@ -113,22 +129,30 @@ function showFreeTierCTA(count) {
113
129
 
114
130
  if (remaining === 0) {
115
131
  // Last free run used — maximum urgency
116
- console.log(` ${RD}${B}Free tier limit reached${R} ${D}(${count}/${limit} runs used today)${R}`);
132
+ console.log(` ${RD}${B}Daily limit reached${R} ${D}(${count}/${limit} free runs used today)${R}`);
117
133
  blank();
118
- console.log(` You've seen what ContentGrade can do. Don't stop here.`);
119
- console.log(` ${B}Pro${R} gives you ${B}20 analyses/day${R} + batch mode for whole directories.`);
134
+ console.log(` ${B}Upgrade to Pro $9/mo${R} to keep going:`);
120
135
  blank();
121
- console.log(` ${MG}${B}→ Upgrade for $9/mo${R} ${CY}${UPGRADE_LINKS.free}${R}`);
136
+ console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} no daily cap`);
137
+ console.log(` ${GN}✓${R} ${B}Batch mode${R} grade an entire directory at once`);
138
+ console.log(` ${GN}✓${R} ${B}URL analysis${R} audit any live page`);
139
+ console.log(` ${GN}✓${R} ${B}Priority support${R} direct email response within 24h`);
140
+ blank();
141
+ console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
142
+ console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
122
143
  blank();
123
144
  console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
124
145
  } else if (remaining === 1) {
125
146
  // 1 run left — build urgency
126
- console.log(` ${YL}${B}${count}/${limit} free runs used today${R} ${D}· 1 remaining${R}`);
147
+ console.log(` ${YL}${B}${count}/${limit} free runs used${R} ${D}· 1 remaining${R}`);
148
+ blank();
149
+ console.log(` Last free run of the day. Pro is ${B}$9/mo${R}:`);
127
150
  blank();
128
- console.log(` One run left on the free tier. ${B}Pro unlocks 20/day${R} + batch analysis.`);
129
- console.log(` ${MG}→ Upgrade for $9/mo:${R} ${CY}${UPGRADE_LINKS.free}${R}`);
151
+ console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} + batch mode + priority support`);
152
+ console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
153
+ console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
130
154
  blank();
131
- console.log(` ${D}Or continue: ${CY}content-grade analyze ./another-post.md${R}`);
155
+ console.log(` ${D}Or use your last run: ${CY}content-grade analyze ./another-post.md${R}`);
132
156
  } else {
133
157
  // 1+ runs left — light, value-focused nudge
134
158
  console.log(` ${D}Free tier: ${count}/${limit} runs used today · ${remaining} remaining${R}`);
@@ -139,12 +163,41 @@ function showFreeTierCTA(count) {
139
163
  console.log(` ${CY}content-grade analyze ./another-post.md${R} ${D}# analyze another file${R}`);
140
164
  console.log(` ${CY}content-grade analyze https://yoursite.com/post${R} ${D}# audit any live URL${R}`);
141
165
  blank();
142
- console.log(` ${MG}Unlock batch mode:${R} ${D}analyze a whole directory at once${R}`);
143
- console.log(` ${D} ${CY}content-grade activate${R} ${D}→ enter your Pro license key ($9/mo)${R}`);
144
- console.log(` ${D} Get one: ${CY}${UPGRADE_LINKS.free}${R}`);
166
+ console.log(` ${MG}Unlock batch mode:${R} grade a whole directory at once`);
167
+ console.log(` ${D} Have a key? ${CY}content-grade activate${R}`);
168
+ console.log(` Get Pro — $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
145
169
  }
146
170
  }
147
171
 
172
+ // Block a run before it starts if the free tier is exhausted.
173
+ // Returns true (blocked) and prints a visually distinct upgrade prompt.
174
+ // Returns false if the user may proceed.
175
+ function checkFreeTierLimit() {
176
+ if (isProUser()) return false;
177
+ const usage = getUsage();
178
+ const limit = TIER_LIMITS.free;
179
+ if (usage.count < limit) return false;
180
+
181
+ blank();
182
+ hr();
183
+ console.log(` ${RD}${B}Daily limit reached${R} ${D}(${usage.count}/${limit} free runs used today)${R}`);
184
+ blank();
185
+ console.log(` ${B}Upgrade to Pro — $9/mo${R} to keep going:`);
186
+ blank();
187
+ console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} no daily cap`);
188
+ console.log(` ${GN}✓${R} ${B}Batch mode${R} grade an entire directory at once`);
189
+ console.log(` ${GN}✓${R} ${B}URL analysis${R} audit any live page`);
190
+ console.log(` ${GN}✓${R} ${B}Priority support${R} direct email response within 24h`);
191
+ blank();
192
+ console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
193
+ console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
194
+ blank();
195
+ console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
196
+ hr();
197
+ blank();
198
+ return true;
199
+ }
200
+
148
201
  let _version = '1.0.0';
149
202
  try { _version = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')).version ?? '1.0.0'; } catch {}
150
203
 
@@ -410,6 +463,8 @@ async function cmdAnalyze(filePath) {
410
463
  process.exit(1);
411
464
  }
412
465
 
466
+ if (checkFreeTierLimit()) { process.exit(1); }
467
+
413
468
  if (!_jsonMode && !_quietMode) {
414
469
  banner();
415
470
  console.log(` ${B}Analyzing:${R} ${CY}${basename(absPath)}${R}`);
@@ -553,8 +608,8 @@ async function cmdAnalyze(filePath) {
553
608
  blank();
554
609
  }
555
610
 
556
- // Track usage
557
- const usageCount = incrementUsage();
611
+ // Track usage — skip entirely for licensed users
612
+ const usageCount = isProUser() ? 0 : incrementUsage();
558
613
 
559
614
  // Save to file if --save flag is set
560
615
  if (_saveMode && result) {
@@ -648,6 +703,8 @@ async function cmdHeadline(text) {
648
703
  process.exit(1);
649
704
  }
650
705
 
706
+ if (checkFreeTierLimit()) { process.exit(1); }
707
+
651
708
  if (!_jsonMode && !_quietMode) {
652
709
  banner();
653
710
  console.log(` ${B}Grading headline:${R}`);
@@ -814,7 +871,7 @@ async function cmdInit() {
814
871
  console.log(` ${CY}content-grade start${R}`);
815
872
  blank();
816
873
  }
817
- console.log(` ${D}Pro tier: 20 analyses/day + batch mode — ${CY}content-grade.onrender.com${R}`);
874
+ console.log(` ${D}Pro tier: unlimited analyses/day + batch mode — ${CY}content-grade.onrender.com${R}`);
818
875
  blank();
819
876
  }
820
877
 
@@ -834,14 +891,14 @@ async function cmdActivate() {
834
891
  blank();
835
892
  console.log(` ${B}Pro features:${R}`);
836
893
  console.log(` ${D} content-grade batch ./posts/ ${R}${D}# analyze all files in a directory${R}`);
837
- console.log(` ${D} 20 analyses/day (vs 3 free)${R}`);
894
+ console.log(` ${D} Unlimited analyses/day (vs 3 free)${R}`);
838
895
  blank();
839
896
  return;
840
897
  }
841
898
 
842
899
  console.log(` ${D}Pro tier unlocks:${R}`);
843
900
  console.log(` ${D} · ${CY}content-grade batch <dir>${R}${D} — analyze entire directories${R}`);
844
- console.log(` ${D} · 20 analyses/day (vs 3 free)${R}`);
901
+ console.log(` ${D} · Unlimited analyses/day (vs 3 free)${R}`);
845
902
  blank();
846
903
  console.log(` ${D}Get a license: ${CY}content-grade.onrender.com${R}${D} → Pricing${R}`);
847
904
  blank();
@@ -890,6 +947,14 @@ async function cmdActivate() {
890
947
  process.exit(1);
891
948
  }
892
949
 
950
+ // Validate key format: CG-XXXX-XXXX-XXXX-XXXX
951
+ if (!isValidKeyFormat(key)) {
952
+ blank();
953
+ fail(`Invalid key format. Expected: CG-XXXX-XXXX-XXXX-XXXX (e.g. CG-A1B2-C3D4-E5F6-A7B8)`);
954
+ blank();
955
+ process.exit(1);
956
+ }
957
+
893
958
  // Validate key against server before storing
894
959
  const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://content-grade.onrender.com';
895
960
  blank();
@@ -936,6 +1001,14 @@ async function cmdActivate() {
936
1001
  config.activatedAt = new Date().toISOString();
937
1002
  saveConfig(config);
938
1003
 
1004
+ // Also write canonical license file to ~/.content-grade/license.json
1005
+ mkdirSync(LICENSE_DIR, { recursive: true });
1006
+ writeFileSync(LICENSE_FILE, JSON.stringify({
1007
+ key,
1008
+ tier: activatedTier,
1009
+ activatedAt: config.activatedAt,
1010
+ }, null, 2), 'utf8');
1011
+
939
1012
  const tierLimit = TIER_LIMITS[activatedTier] || TIER_LIMITS.free;
940
1013
  blank();
941
1014
  ok(`${(TIER_NAMES[activatedTier] || activatedTier).split(' —')[0]} activated! Your daily limit is now ${tierLimit} analyses.`);
@@ -957,10 +1030,16 @@ async function cmdBatch(dirPath) {
957
1030
  console.log(` ${D}Free tier: analyze files one at a time.${R}`);
958
1031
  console.log(` ${CY}content-grade analyze ./post.md${R}`);
959
1032
  blank();
960
- console.log(` ${B}Unlock batch mode:${R}`);
961
- console.log(` ${CY}content-grade activate${R} ${D}(enter your license key)${R}`);
1033
+ console.log(` ${B}Upgrade to Pro — $9/mo${R} to unlock batch mode:`);
962
1034
  blank();
963
- console.log(` ${D}Get a license: ${CY}content-grade.onrender.com${R}`);
1035
+ console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} no daily cap`);
1036
+ console.log(` ${GN}✓${R} ${B}Batch mode${R} grade an entire directory at once`);
1037
+ console.log(` ${GN}✓${R} ${B}Priority support${R} direct email response within 24h`);
1038
+ blank();
1039
+ console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
1040
+ console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
1041
+ blank();
1042
+ console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
964
1043
  blank();
965
1044
  process.exit(1);
966
1045
  }
@@ -1063,7 +1142,7 @@ async function cmdBatch(dirPath) {
1063
1142
  'claude-haiku-4-5-20251001'
1064
1143
  );
1065
1144
  const r = parseJSON(raw);
1066
- incrementUsage();
1145
+ if (!isProUser()) incrementUsage();
1067
1146
  const sc = r.total_score;
1068
1147
  const scoreColor = sc >= 70 ? GN : sc >= 45 ? YL : RD;
1069
1148
  process.stdout.write(`\r ${scoreColor}${String(sc).padStart(3)}/100${R} ${gradeLetter(sc)} ${rel}${' '.repeat(10)}\n`);
@@ -1438,7 +1517,7 @@ function cmdHelp() {
1438
1517
  console.log(` ${B}PRICING${R}`);
1439
1518
  blank();
1440
1519
  console.log(` Free 3/day $0`);
1441
- console.log(` Pro 20/day $9/mo`);
1520
+ console.log(` Pro Unlimited $9/mo`);
1442
1521
  console.log(` Business 100/day $29/mo`);
1443
1522
  console.log(` Team 500/day $79/mo`);
1444
1523
  blank();
@@ -1611,10 +1690,9 @@ async function cmdDemo() {
1611
1690
  console.log(` ${CY}npx content-grade headline "Your title here"${R} ${D}# grade a single headline${R}`);
1612
1691
  blank();
1613
1692
 
1614
- // Show telemetry notice after user has seen value (first run only)
1693
+ // Show telemetry notice on first run (opt-out model)
1615
1694
  if (_showTelemNotice) {
1616
- console.log(` ${D}Help improve ContentGradeenable anonymous usage analytics: ${CY}content-grade telemetry on${R}`);
1617
- console.log(` ${D}No PII, no file contents. View what's collected: ${CY}content-grade telemetry${R}`);
1695
+ console.log(` ${D}Anonymous usage analytics enabled no PII, no file contents. Opt out: ${CY}content-grade telemetry off${R}`);
1618
1696
  blank();
1619
1697
  }
1620
1698
 
@@ -2060,7 +2138,7 @@ if (_rawArgs.includes('--help') || _rawArgs.includes('-h')) {
2060
2138
  }
2061
2139
 
2062
2140
  if (_demoMode) {
2063
- recordEvent({ event: 'command', command: 'demo' });
2141
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'demo' });
2064
2142
  cmdDemo().catch(err => {
2065
2143
  blank();
2066
2144
  fail(`Demo error: ${err.message}`);
@@ -2072,7 +2150,7 @@ if (_demoMode) {
2072
2150
  case 'analyse':
2073
2151
  case 'check':
2074
2152
  case 'grade':
2075
- recordEvent({ event: 'command', command: 'analyze' });
2153
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze' });
2076
2154
  cmdAnalyze(args[1]).catch(err => {
2077
2155
  blank();
2078
2156
  fail(`Unexpected error: ${err.message}`);
@@ -2082,7 +2160,7 @@ if (_demoMode) {
2082
2160
  break;
2083
2161
 
2084
2162
  case 'demo':
2085
- recordEvent({ event: 'command', command: 'demo' });
2163
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'demo' });
2086
2164
  cmdDemo().catch(err => {
2087
2165
  blank();
2088
2166
  fail(`Demo error: ${err.message}`);
@@ -2092,7 +2170,7 @@ if (_demoMode) {
2092
2170
  break;
2093
2171
 
2094
2172
  case 'headline':
2095
- recordEvent({ event: 'command', command: 'headline' });
2173
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'headline' });
2096
2174
  cmdHeadline(args.slice(1).join(' ')).catch(err => {
2097
2175
  blank();
2098
2176
  fail(`Unexpected error: ${err.message}`);
@@ -2103,13 +2181,13 @@ if (_demoMode) {
2103
2181
 
2104
2182
  case 'start':
2105
2183
  case 'serve':
2106
- recordEvent({ event: 'command', command: 'start' });
2184
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'start' });
2107
2185
  cmdStart();
2108
2186
  break;
2109
2187
 
2110
2188
  case 'init':
2111
2189
  case 'setup':
2112
- recordEvent({ event: 'command', command: 'init' });
2190
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'init' });
2113
2191
  cmdInit().catch(err => {
2114
2192
  blank();
2115
2193
  fail(`Setup error: ${err.message}`);
@@ -2120,7 +2198,7 @@ if (_demoMode) {
2120
2198
 
2121
2199
  case 'activate':
2122
2200
  case 'license':
2123
- recordEvent({ event: 'command', command: 'activate' });
2201
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'activate' });
2124
2202
  cmdActivate().catch(err => {
2125
2203
  blank();
2126
2204
  fail(`Activation error: ${err.message}`);
@@ -2131,7 +2209,7 @@ if (_demoMode) {
2131
2209
 
2132
2210
  case 'seo-audit':
2133
2211
  case 'seoaudit':
2134
- recordEvent({ event: 'command', command: 'seo-audit' });
2212
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'seo-audit' });
2135
2213
  if (!args[1]) {
2136
2214
  blank();
2137
2215
  fail(`No URL specified.`);
@@ -2150,7 +2228,7 @@ if (_demoMode) {
2150
2228
  break;
2151
2229
 
2152
2230
  case 'batch':
2153
- recordEvent({ event: 'command', command: 'batch' });
2231
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'batch' });
2154
2232
  cmdBatch(args[1]).catch(err => {
2155
2233
  blank();
2156
2234
  fail(`Batch error: ${err.message}`);
@@ -2168,7 +2246,7 @@ if (_demoMode) {
2168
2246
  case 'check-updates':
2169
2247
  case 'update':
2170
2248
  case 'upgrade-check':
2171
- recordEvent({ event: 'command', command: 'check-updates' });
2249
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'check-updates' });
2172
2250
  cmdCheckUpdates().catch(err => {
2173
2251
  blank();
2174
2252
  fail(`Update check error: ${err.message}`);
@@ -2223,13 +2301,13 @@ if (_demoMode) {
2223
2301
 
2224
2302
  case undefined:
2225
2303
  // No command — show instant static demo (no Claude required, zero wait)
2226
- recordEvent({ event: 'command', command: 'quick_demo' });
2304
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'quick_demo' });
2227
2305
  cmdQuickDemo();
2228
2306
  break;
2229
2307
 
2230
2308
  default:
2231
2309
  if (/^https?:\/\//i.test(raw)) {
2232
- recordEvent({ event: 'command', command: 'analyze_url' });
2310
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze_url' });
2233
2311
  cmdAnalyzeUrl(raw).catch(err => {
2234
2312
  blank();
2235
2313
  fail(`Unexpected error: ${err.message}`);
@@ -2263,7 +2341,7 @@ if (_demoMode) {
2263
2341
  } catch {
2264
2342
  target = raw; // let cmdAnalyze handle the "not found" error
2265
2343
  }
2266
- recordEvent({ event: 'command', command: 'analyze' });
2344
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze' });
2267
2345
  cmdAnalyze(target).catch(err => {
2268
2346
  blank();
2269
2347
  fail(`Unexpected error: ${err.message}`);
@@ -2276,7 +2354,7 @@ if (_demoMode) {
2276
2354
  // npx content-grade Why most startups fail
2277
2355
  const allText = args.join(' ').trim();
2278
2356
  if (allText && allText.length >= 5 && !allText.startsWith('-')) {
2279
- recordEvent({ event: 'command', command: 'headline_smart' });
2357
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'headline_smart' });
2280
2358
  cmdHeadline(allText).catch(err => {
2281
2359
  blank();
2282
2360
  fail(`Unexpected error: ${err.message}`);
package/bin/telemetry.js CHANGED
@@ -20,7 +20,7 @@ const EVENTS_FILE = join(CONFIG_DIR, 'events.jsonl');
20
20
  // Telemetry endpoint: CLI events are forwarded here when telemetry is enabled.
21
21
  // Set CONTENT_GRADE_TELEMETRY_URL to enable remote aggregation (e.g. self-hosted endpoint).
22
22
  // Default: local-only (events stored at ~/.content-grade/events.jsonl, no remote send).
23
- const REMOTE_URL = process.env.CONTENT_GRADE_TELEMETRY_URL || 'https://cg-telemetry.onrender.com/api/telemetry/events';
23
+ const REMOTE_URL = process.env.CONTENT_GRADE_TELEMETRY_URL || 'https://content-grade.onrender.com/api/telemetry';
24
24
 
25
25
  // ── Internal helpers ──────────────────────────────────────────────────────────
26
26
 
@@ -72,13 +72,13 @@ export function initTelemetry() {
72
72
  return { installId: cfg.installId, isNew: false, optedOut: false };
73
73
  }
74
74
 
75
- // First run — generate anonymous install ID, opt-out by default (privacy-first)
76
- // Users can opt in: content-grade telemetry on
75
+ // First run — generate anonymous install ID, enabled by default (opt-out available)
76
+ // Users can opt out: content-grade telemetry off OR --no-telemetry flag
77
77
  const installId = generateId();
78
78
  saveConfig({
79
79
  installId,
80
80
  installedAt: new Date().toISOString(),
81
- telemetryEnabled: false,
81
+ telemetryEnabled: true,
82
82
  });
83
83
 
84
84
  return { installId, isNew: true, optedOut: false };