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 +61 -11
- package/README.md +14 -0
- package/bin/content-grade.js +124 -46
- package/bin/telemetry.js +4 -4
- package/dist/landing.html +1477 -0
- package/dist/privacy.html +189 -0
- package/dist/terms.html +185 -0
- package/dist-server/server/db.js +6 -0
- package/dist-server/server/index.js +2 -0
- package/dist-server/server/routes/analytics.js +3 -3
- package/dist-server/server/routes/demos.js +10 -8
- package/dist-server/server/routes/stripe.js +82 -3
- package/package.json +1 -1
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
|
-
##
|
|
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
|
-
|
|
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
|
|
19
|
-
3. Run the full check
|
|
57
|
+
2. Make your change
|
|
58
|
+
3. Run the full check:
|
|
20
59
|
|
|
21
60
|
```bash
|
|
22
|
-
npm test
|
|
23
|
-
npm run typecheck
|
|
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
|
-
|
|
68
|
+
The PR template has a checklist — fill it out. Small, focused PRs review faster.
|
|
29
69
|
|
|
30
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
+
[](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)
|
package/bin/content-grade.js
CHANGED
|
@@ -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
|
-
|
|
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://
|
|
79
|
-
pro: 'https://
|
|
80
|
-
business: 'https://
|
|
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:
|
|
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
|
|
100
|
-
if (tier === 'pro') return `Upgrade to Business
|
|
101
|
-
if (tier === 'business') return `Upgrade to Team
|
|
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}
|
|
132
|
+
console.log(` ${RD}${B}Daily limit reached${R} ${D}(${count}/${limit} free runs used today)${R}`);
|
|
117
133
|
blank();
|
|
118
|
-
console.log(`
|
|
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(` ${
|
|
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
|
|
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(`
|
|
129
|
-
console.log(` ${MG}→ Upgrade
|
|
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
|
|
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}
|
|
143
|
-
console.log(` ${D} ${CY}content-grade activate${R}
|
|
144
|
-
console.log(`
|
|
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:
|
|
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}
|
|
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} ·
|
|
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}
|
|
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(` ${
|
|
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
|
|
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
|
|
1693
|
+
// Show telemetry notice on first run (opt-out model)
|
|
1615
1694
|
if (_showTelemNotice) {
|
|
1616
|
-
console.log(` ${D}
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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://
|
|
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,
|
|
76
|
-
// Users can opt
|
|
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:
|
|
81
|
+
telemetryEnabled: true,
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
return { installId, isNew: true, optedOut: false };
|