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 +61 -11
- package/README.md +14 -0
- package/bin/content-grade.js +107 -43
- 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,23 @@ 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}
|
|
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
|
|
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
|
|
143
|
+
console.log(` ${YL}${B}${count}/${limit} free runs used${R} ${D}· 1 remaining${R}`);
|
|
127
144
|
blank();
|
|
128
|
-
console.log(`
|
|
129
|
-
console.log(` ${MG}→ Upgrade
|
|
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
|
|
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}
|
|
143
|
-
console.log(` ${D} ${CY}content-grade activate${R}
|
|
144
|
-
console.log(`
|
|
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:
|
|
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}
|
|
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} ·
|
|
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
|
|
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
|
|
1679
|
+
// Show telemetry notice on first run (opt-out model)
|
|
1615
1680
|
if (_showTelemNotice) {
|
|
1616
|
-
console.log(` ${D}
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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://
|
|
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 };
|