content-grade 1.0.19 → 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,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,23 @@ 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(` 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.`);
120
136
  blank();
121
- console.log(` ${MG}${B}→ Upgrade for $9/mo${R} ${CY}${UPGRADE_LINKS.free}${R}`);
137
+ console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
138
+ console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
122
139
  blank();
123
140
  console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
124
141
  } else if (remaining === 1) {
125
142
  // 1 run left — build urgency
126
- console.log(` ${YL}${B}${count}/${limit} free runs used today${R} ${D}· 1 remaining${R}`);
143
+ console.log(` ${YL}${B}${count}/${limit} free runs used${R} ${D}· 1 remaining${R}`);
127
144
  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}`);
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}`);
130
147
  blank();
131
- console.log(` ${D}Or continue: ${CY}content-grade analyze ./another-post.md${R}`);
148
+ console.log(` ${D}Or use your last run: ${CY}content-grade analyze ./another-post.md${R}`);
132
149
  } else {
133
150
  // 1+ runs left — light, value-focused nudge
134
151
  console.log(` ${D}Free tier: ${count}/${limit} runs used today · ${remaining} remaining${R}`);
@@ -139,12 +156,40 @@ function showFreeTierCTA(count) {
139
156
  console.log(` ${CY}content-grade analyze ./another-post.md${R} ${D}# analyze another file${R}`);
140
157
  console.log(` ${CY}content-grade analyze https://yoursite.com/post${R} ${D}# audit any live URL${R}`);
141
158
  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}`);
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}`);
145
162
  }
146
163
  }
147
164
 
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;
191
+ }
192
+
148
193
  let _version = '1.0.0';
149
194
  try { _version = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')).version ?? '1.0.0'; } catch {}
150
195
 
@@ -410,6 +455,8 @@ async function cmdAnalyze(filePath) {
410
455
  process.exit(1);
411
456
  }
412
457
 
458
+ if (checkFreeTierLimit()) { process.exit(1); }
459
+
413
460
  if (!_jsonMode && !_quietMode) {
414
461
  banner();
415
462
  console.log(` ${B}Analyzing:${R} ${CY}${basename(absPath)}${R}`);
@@ -553,8 +600,8 @@ async function cmdAnalyze(filePath) {
553
600
  blank();
554
601
  }
555
602
 
556
- // Track usage
557
- const usageCount = incrementUsage();
603
+ // Track usage — skip entirely for licensed users
604
+ const usageCount = isProUser() ? 0 : incrementUsage();
558
605
 
559
606
  // Save to file if --save flag is set
560
607
  if (_saveMode && result) {
@@ -648,6 +695,8 @@ async function cmdHeadline(text) {
648
695
  process.exit(1);
649
696
  }
650
697
 
698
+ if (checkFreeTierLimit()) { process.exit(1); }
699
+
651
700
  if (!_jsonMode && !_quietMode) {
652
701
  banner();
653
702
  console.log(` ${B}Grading headline:${R}`);
@@ -814,7 +863,7 @@ async function cmdInit() {
814
863
  console.log(` ${CY}content-grade start${R}`);
815
864
  blank();
816
865
  }
817
- console.log(` ${D}Pro tier: 20 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}`);
818
867
  blank();
819
868
  }
820
869
 
@@ -834,14 +883,14 @@ async function cmdActivate() {
834
883
  blank();
835
884
  console.log(` ${B}Pro features:${R}`);
836
885
  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}`);
886
+ console.log(` ${D} Unlimited analyses/day (vs 3 free)${R}`);
838
887
  blank();
839
888
  return;
840
889
  }
841
890
 
842
891
  console.log(` ${D}Pro tier unlocks:${R}`);
843
892
  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}`);
893
+ console.log(` ${D} · Unlimited analyses/day (vs 3 free)${R}`);
845
894
  blank();
846
895
  console.log(` ${D}Get a license: ${CY}content-grade.onrender.com${R}${D} → Pricing${R}`);
847
896
  blank();
@@ -890,6 +939,14 @@ async function cmdActivate() {
890
939
  process.exit(1);
891
940
  }
892
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
+
893
950
  // Validate key against server before storing
894
951
  const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://content-grade.onrender.com';
895
952
  blank();
@@ -936,6 +993,14 @@ async function cmdActivate() {
936
993
  config.activatedAt = new Date().toISOString();
937
994
  saveConfig(config);
938
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
+
939
1004
  const tierLimit = TIER_LIMITS[activatedTier] || TIER_LIMITS.free;
940
1005
  blank();
941
1006
  ok(`${(TIER_NAMES[activatedTier] || activatedTier).split(' —')[0]} activated! Your daily limit is now ${tierLimit} analyses.`);
@@ -1063,7 +1128,7 @@ async function cmdBatch(dirPath) {
1063
1128
  'claude-haiku-4-5-20251001'
1064
1129
  );
1065
1130
  const r = parseJSON(raw);
1066
- incrementUsage();
1131
+ if (!isProUser()) incrementUsage();
1067
1132
  const sc = r.total_score;
1068
1133
  const scoreColor = sc >= 70 ? GN : sc >= 45 ? YL : RD;
1069
1134
  process.stdout.write(`\r ${scoreColor}${String(sc).padStart(3)}/100${R} ${gradeLetter(sc)} ${rel}${' '.repeat(10)}\n`);
@@ -1438,7 +1503,7 @@ function cmdHelp() {
1438
1503
  console.log(` ${B}PRICING${R}`);
1439
1504
  blank();
1440
1505
  console.log(` Free 3/day $0`);
1441
- console.log(` Pro 20/day $9/mo`);
1506
+ console.log(` Pro Unlimited $9/mo`);
1442
1507
  console.log(` Business 100/day $29/mo`);
1443
1508
  console.log(` Team 500/day $79/mo`);
1444
1509
  blank();
@@ -1611,10 +1676,9 @@ async function cmdDemo() {
1611
1676
  console.log(` ${CY}npx content-grade headline "Your title here"${R} ${D}# grade a single headline${R}`);
1612
1677
  blank();
1613
1678
 
1614
- // Show telemetry notice after user has seen value (first run only)
1679
+ // Show telemetry notice on first run (opt-out model)
1615
1680
  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}`);
1681
+ console.log(` ${D}Anonymous usage analytics enabled no PII, no file contents. Opt out: ${CY}content-grade telemetry off${R}`);
1618
1682
  blank();
1619
1683
  }
1620
1684
 
@@ -2060,7 +2124,7 @@ if (_rawArgs.includes('--help') || _rawArgs.includes('-h')) {
2060
2124
  }
2061
2125
 
2062
2126
  if (_demoMode) {
2063
- recordEvent({ event: 'command', command: 'demo' });
2127
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'demo' });
2064
2128
  cmdDemo().catch(err => {
2065
2129
  blank();
2066
2130
  fail(`Demo error: ${err.message}`);
@@ -2072,7 +2136,7 @@ if (_demoMode) {
2072
2136
  case 'analyse':
2073
2137
  case 'check':
2074
2138
  case 'grade':
2075
- recordEvent({ event: 'command', command: 'analyze' });
2139
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze' });
2076
2140
  cmdAnalyze(args[1]).catch(err => {
2077
2141
  blank();
2078
2142
  fail(`Unexpected error: ${err.message}`);
@@ -2082,7 +2146,7 @@ if (_demoMode) {
2082
2146
  break;
2083
2147
 
2084
2148
  case 'demo':
2085
- recordEvent({ event: 'command', command: 'demo' });
2149
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'demo' });
2086
2150
  cmdDemo().catch(err => {
2087
2151
  blank();
2088
2152
  fail(`Demo error: ${err.message}`);
@@ -2092,7 +2156,7 @@ if (_demoMode) {
2092
2156
  break;
2093
2157
 
2094
2158
  case 'headline':
2095
- recordEvent({ event: 'command', command: 'headline' });
2159
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'headline' });
2096
2160
  cmdHeadline(args.slice(1).join(' ')).catch(err => {
2097
2161
  blank();
2098
2162
  fail(`Unexpected error: ${err.message}`);
@@ -2103,13 +2167,13 @@ if (_demoMode) {
2103
2167
 
2104
2168
  case 'start':
2105
2169
  case 'serve':
2106
- recordEvent({ event: 'command', command: 'start' });
2170
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'start' });
2107
2171
  cmdStart();
2108
2172
  break;
2109
2173
 
2110
2174
  case 'init':
2111
2175
  case 'setup':
2112
- recordEvent({ event: 'command', command: 'init' });
2176
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'init' });
2113
2177
  cmdInit().catch(err => {
2114
2178
  blank();
2115
2179
  fail(`Setup error: ${err.message}`);
@@ -2120,7 +2184,7 @@ if (_demoMode) {
2120
2184
 
2121
2185
  case 'activate':
2122
2186
  case 'license':
2123
- recordEvent({ event: 'command', command: 'activate' });
2187
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'activate' });
2124
2188
  cmdActivate().catch(err => {
2125
2189
  blank();
2126
2190
  fail(`Activation error: ${err.message}`);
@@ -2131,7 +2195,7 @@ if (_demoMode) {
2131
2195
 
2132
2196
  case 'seo-audit':
2133
2197
  case 'seoaudit':
2134
- recordEvent({ event: 'command', command: 'seo-audit' });
2198
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'seo-audit' });
2135
2199
  if (!args[1]) {
2136
2200
  blank();
2137
2201
  fail(`No URL specified.`);
@@ -2150,7 +2214,7 @@ if (_demoMode) {
2150
2214
  break;
2151
2215
 
2152
2216
  case 'batch':
2153
- recordEvent({ event: 'command', command: 'batch' });
2217
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'batch' });
2154
2218
  cmdBatch(args[1]).catch(err => {
2155
2219
  blank();
2156
2220
  fail(`Batch error: ${err.message}`);
@@ -2168,7 +2232,7 @@ if (_demoMode) {
2168
2232
  case 'check-updates':
2169
2233
  case 'update':
2170
2234
  case 'upgrade-check':
2171
- recordEvent({ event: 'command', command: 'check-updates' });
2235
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'check-updates' });
2172
2236
  cmdCheckUpdates().catch(err => {
2173
2237
  blank();
2174
2238
  fail(`Update check error: ${err.message}`);
@@ -2223,13 +2287,13 @@ if (_demoMode) {
2223
2287
 
2224
2288
  case undefined:
2225
2289
  // No command — show instant static demo (no Claude required, zero wait)
2226
- recordEvent({ event: 'command', command: 'quick_demo' });
2290
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'quick_demo' });
2227
2291
  cmdQuickDemo();
2228
2292
  break;
2229
2293
 
2230
2294
  default:
2231
2295
  if (/^https?:\/\//i.test(raw)) {
2232
- recordEvent({ event: 'command', command: 'analyze_url' });
2296
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze_url' });
2233
2297
  cmdAnalyzeUrl(raw).catch(err => {
2234
2298
  blank();
2235
2299
  fail(`Unexpected error: ${err.message}`);
@@ -2263,7 +2327,7 @@ if (_demoMode) {
2263
2327
  } catch {
2264
2328
  target = raw; // let cmdAnalyze handle the "not found" error
2265
2329
  }
2266
- recordEvent({ event: 'command', command: 'analyze' });
2330
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze' });
2267
2331
  cmdAnalyze(target).catch(err => {
2268
2332
  blank();
2269
2333
  fail(`Unexpected error: ${err.message}`);
@@ -2276,7 +2340,7 @@ if (_demoMode) {
2276
2340
  // npx content-grade Why most startups fail
2277
2341
  const allText = args.join(' ').trim();
2278
2342
  if (allText && allText.length >= 5 && !allText.startsWith('-')) {
2279
- recordEvent({ event: 'command', command: 'headline_smart' });
2343
+ recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'headline_smart' });
2280
2344
  cmdHeadline(allText).catch(err => {
2281
2345
  blank();
2282
2346
  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 };