content-grade 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +61 -11
- package/README.md +14 -0
- package/bin/content-grade.js +160 -107
- package/bin/telemetry.js +16 -10
- package/dist/landing.html +41 -28
- package/dist-server/server/db.js +9 -1
- package/dist-server/server/index.js +18 -2
- package/dist-server/server/routes/analytics.js +3 -3
- package/dist-server/server/routes/demos.js +57 -28
- package/dist-server/server/routes/stripe.js +102 -5
- package/dist-server/server/services/stripe.js +40 -11
- 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,12 +52,21 @@ 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() {
|
|
50
66
|
const cfg = loadConfig();
|
|
51
|
-
|
|
67
|
+
const tier = cfg.tier || 'free';
|
|
68
|
+
// Collapse legacy 'starter' tier to 'pro' — Starter was removed
|
|
69
|
+
return tier === 'starter' ? 'pro' : tier;
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
function isProUser() { return Boolean(getLicenseKey()); }
|
|
@@ -71,45 +89,105 @@ function incrementUsage() {
|
|
|
71
89
|
return u.count;
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
//
|
|
75
|
-
const TIER_LIMITS = {
|
|
76
|
-
free: 5, // 5/day — try it out
|
|
77
|
-
starter: 10, // 10/day — $0.030/run
|
|
78
|
-
pro: 50, // 50/day — $0.019/run (best value per run)
|
|
79
|
-
team: 200, // 200/day — $0.013/run (bulk)
|
|
80
|
-
};
|
|
81
|
-
|
|
92
|
+
// Upgrade links — Free → Pro → Business → Team
|
|
82
93
|
const UPGRADE_LINKS = {
|
|
83
|
-
free:
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
free: 'https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a', // Pro $9/mo — direct checkout
|
|
95
|
+
pro: 'https://buy.stripe.com/bJefZjafO2Tz36Z2W48k80b', // Business $29/mo
|
|
96
|
+
business: 'https://buy.stripe.com/cNiaEZfA8cu9bDv4088k80c', // Team $79/mo
|
|
86
97
|
};
|
|
87
98
|
|
|
88
99
|
const TIER_NAMES = {
|
|
89
|
-
free:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
team:
|
|
100
|
+
free: 'Free',
|
|
101
|
+
pro: 'Pro',
|
|
102
|
+
business: 'Business',
|
|
103
|
+
team: 'Team',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const TIER_LIMITS = {
|
|
107
|
+
free: 3,
|
|
108
|
+
pro: Infinity,
|
|
109
|
+
business: 100,
|
|
110
|
+
team: 500,
|
|
93
111
|
};
|
|
94
112
|
|
|
95
|
-
function
|
|
113
|
+
function getUpgradeMessage() {
|
|
96
114
|
const tier = getLicenseTier();
|
|
97
|
-
|
|
115
|
+
if (tier === 'free') return `Upgrade to Pro — unlimited analyses for $9/mo → ${UPGRADE_LINKS.free}`;
|
|
116
|
+
if (tier === 'pro') return `Upgrade to Business — 100 analyses/day for $29/mo → ${UPGRADE_LINKS.pro}`;
|
|
117
|
+
if (tier === 'business') return `Upgrade to Team — 500 analyses/day for $79/mo → ${UPGRADE_LINKS.business}`;
|
|
118
|
+
return '';
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
// Show usage-aware upgrade CTA after each free run.
|
|
122
|
+
// count = today's usage AFTER this run (1 = first run, 3 = last free run).
|
|
123
|
+
function showFreeTierCTA(count) {
|
|
124
|
+
const limit = TIER_LIMITS.free; // 3
|
|
125
|
+
const remaining = Math.max(0, limit - count);
|
|
126
|
+
|
|
127
|
+
blank();
|
|
128
|
+
hr();
|
|
129
|
+
|
|
130
|
+
if (remaining === 0) {
|
|
131
|
+
// Last free run used — maximum urgency
|
|
132
|
+
console.log(` ${RD}${B}Daily limit reached${R} ${D}(${count}/${limit} free runs used today)${R}`);
|
|
133
|
+
blank();
|
|
134
|
+
console.log(` That's ${count} analyses today. Pro is ${B}$9/mo${R} — run as many as you need.`);
|
|
135
|
+
console.log(` ${B}Unlimited + batch mode${R} — grade an entire directory in one command.`);
|
|
136
|
+
blank();
|
|
137
|
+
console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
|
|
138
|
+
console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
|
|
139
|
+
blank();
|
|
140
|
+
console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
|
|
141
|
+
} else if (remaining === 1) {
|
|
142
|
+
// 1 run left — build urgency
|
|
143
|
+
console.log(` ${YL}${B}${count}/${limit} free runs used${R} ${D}· 1 remaining${R}`);
|
|
144
|
+
blank();
|
|
145
|
+
console.log(` Last free run of the day. Pro is ${B}$9/mo${R} — unlimited + batch mode.`);
|
|
146
|
+
console.log(` ${MG}${B}→ Upgrade now:${R} ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
147
|
+
blank();
|
|
148
|
+
console.log(` ${D}Or use your last run: ${CY}content-grade analyze ./another-post.md${R}`);
|
|
149
|
+
} else {
|
|
150
|
+
// 1+ runs left — light, value-focused nudge
|
|
151
|
+
console.log(` ${D}Free tier: ${count}/${limit} runs used today · ${remaining} remaining${R}`);
|
|
152
|
+
blank();
|
|
153
|
+
console.log(` ${B}What to do next:${R}`);
|
|
154
|
+
blank();
|
|
155
|
+
console.log(` ${CY}content-grade headline "Your title here"${R} ${D}# grade a headline in ~5s${R}`);
|
|
156
|
+
console.log(` ${CY}content-grade analyze ./another-post.md${R} ${D}# analyze another file${R}`);
|
|
157
|
+
console.log(` ${CY}content-grade analyze https://yoursite.com/post${R} ${D}# audit any live URL${R}`);
|
|
158
|
+
blank();
|
|
159
|
+
console.log(` ${MG}Unlock batch mode:${R} grade a whole directory at once`);
|
|
160
|
+
console.log(` ${D} Have a key? ${CY}content-grade activate${R}`);
|
|
161
|
+
console.log(` Get Pro — $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
162
|
+
}
|
|
105
163
|
}
|
|
106
164
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
165
|
+
// Block a run before it starts if the free tier is exhausted.
|
|
166
|
+
// Returns true (blocked) and prints a visually distinct upgrade prompt.
|
|
167
|
+
// Returns false if the user may proceed.
|
|
168
|
+
function checkFreeTierLimit() {
|
|
169
|
+
if (isProUser()) return false;
|
|
170
|
+
const usage = getUsage();
|
|
171
|
+
const limit = TIER_LIMITS.free;
|
|
172
|
+
if (usage.count < limit) return false;
|
|
173
|
+
|
|
174
|
+
blank();
|
|
175
|
+
hr();
|
|
176
|
+
console.log(` ${RD}${B}Daily limit reached${R} ${D}(${usage.count}/${limit} free runs used today)${R}`);
|
|
177
|
+
blank();
|
|
178
|
+
console.log(` ${B}Upgrade to Pro — $9/mo${R} to keep going:`);
|
|
179
|
+
blank();
|
|
180
|
+
console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} no daily cap`);
|
|
181
|
+
console.log(` ${GN}✓${R} ${B}Batch mode${R} grade an entire directory at once`);
|
|
182
|
+
console.log(` ${GN}✓${R} ${B}URL analysis${R} audit any live page`);
|
|
183
|
+
blank();
|
|
184
|
+
console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
|
|
185
|
+
console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
|
|
186
|
+
blank();
|
|
187
|
+
console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
|
|
188
|
+
hr();
|
|
189
|
+
blank();
|
|
190
|
+
return true;
|
|
113
191
|
}
|
|
114
192
|
|
|
115
193
|
let _version = '1.0.0';
|
|
@@ -377,20 +455,7 @@ async function cmdAnalyze(filePath) {
|
|
|
377
455
|
process.exit(1);
|
|
378
456
|
}
|
|
379
457
|
|
|
380
|
-
|
|
381
|
-
const limitCheck = checkDailyLimit();
|
|
382
|
-
if (!limitCheck.ok) {
|
|
383
|
-
blank();
|
|
384
|
-
fail(`Daily limit reached (${limitCheck.count}/${limitCheck.limit} runs used today on ${TIER_NAMES[getLicenseTier()]} plan).`);
|
|
385
|
-
blank();
|
|
386
|
-
console.log(` ${B}Options:${R}`);
|
|
387
|
-
console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
|
|
388
|
-
console.log(` ${D}· Upgrade your plan: ${CY}content-grade activate${R}`);
|
|
389
|
-
blank();
|
|
390
|
-
console.log(` ${MG}${getUpgradeMessage()}${R}`);
|
|
391
|
-
blank();
|
|
392
|
-
process.exit(1);
|
|
393
|
-
}
|
|
458
|
+
if (checkFreeTierLimit()) { process.exit(1); }
|
|
394
459
|
|
|
395
460
|
if (!_jsonMode && !_quietMode) {
|
|
396
461
|
banner();
|
|
@@ -535,8 +600,8 @@ async function cmdAnalyze(filePath) {
|
|
|
535
600
|
blank();
|
|
536
601
|
}
|
|
537
602
|
|
|
538
|
-
// Track usage
|
|
539
|
-
incrementUsage();
|
|
603
|
+
// Track usage — skip entirely for licensed users
|
|
604
|
+
const usageCount = isProUser() ? 0 : incrementUsage();
|
|
540
605
|
|
|
541
606
|
// Save to file if --save flag is set
|
|
542
607
|
if (_saveMode && result) {
|
|
@@ -551,25 +616,13 @@ async function cmdAnalyze(filePath) {
|
|
|
551
616
|
}
|
|
552
617
|
}
|
|
553
618
|
|
|
554
|
-
// Next step —
|
|
555
|
-
blank();
|
|
556
|
-
hr();
|
|
619
|
+
// Next step — usage-aware CTA after user has seen value
|
|
557
620
|
if (isProUser()) {
|
|
621
|
+
blank();
|
|
622
|
+
hr();
|
|
558
623
|
console.log(` ${D}Pro active · Next: ${CY}content-grade batch ./posts/${R}${D} to analyze a whole directory${R}`);
|
|
559
624
|
} else {
|
|
560
|
-
|
|
561
|
-
const remaining = Math.max(0, FREE_DAILY_LIMIT - usage.count);
|
|
562
|
-
console.log(` ${B}What to do next:${R}`);
|
|
563
|
-
blank();
|
|
564
|
-
console.log(` ${CY}content-grade headline "Your title here"${R} ${D}# grade a headline in ~5s${R}`);
|
|
565
|
-
console.log(` ${CY}content-grade analyze ./another-post.md${R} ${D}# analyze another file${R}`);
|
|
566
|
-
console.log(` ${CY}content-grade analyze https://yoursite.com/post${R} ${D}# audit any live URL${R}`);
|
|
567
|
-
blank();
|
|
568
|
-
if (remaining <= 10) {
|
|
569
|
-
console.log(` ${YL}${remaining} runs left today (${TIER_NAMES[getLicenseTier()]}).${R} ${MG}${getUpgradeMessage()}${R}`);
|
|
570
|
-
} else {
|
|
571
|
-
console.log(` ${D}${remaining} runs left today. ${getUpgradeMessage()}${R}`);
|
|
572
|
-
}
|
|
625
|
+
showFreeTierCTA(usageCount);
|
|
573
626
|
}
|
|
574
627
|
|
|
575
628
|
// CI exit code — shown after full output so user sees the score before exit
|
|
@@ -642,20 +695,7 @@ async function cmdHeadline(text) {
|
|
|
642
695
|
process.exit(1);
|
|
643
696
|
}
|
|
644
697
|
|
|
645
|
-
|
|
646
|
-
const limitCheck = checkDailyLimit();
|
|
647
|
-
if (!limitCheck.ok) {
|
|
648
|
-
blank();
|
|
649
|
-
fail(`Daily limit reached (${limitCheck.count}/${limitCheck.limit} runs used today on ${TIER_NAMES[getLicenseTier()]} plan).`);
|
|
650
|
-
blank();
|
|
651
|
-
console.log(` ${B}Options:${R}`);
|
|
652
|
-
console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
|
|
653
|
-
console.log(` ${D}· Upgrade your plan: ${CY}content-grade activate${R}`);
|
|
654
|
-
blank();
|
|
655
|
-
console.log(` ${MG}${getUpgradeMessage()}${R}`);
|
|
656
|
-
blank();
|
|
657
|
-
process.exit(1);
|
|
658
|
-
}
|
|
698
|
+
if (checkFreeTierLimit()) { process.exit(1); }
|
|
659
699
|
|
|
660
700
|
if (!_jsonMode && !_quietMode) {
|
|
661
701
|
banner();
|
|
@@ -742,8 +782,8 @@ async function cmdHeadline(text) {
|
|
|
742
782
|
console.log(` ${D}Compare two headlines: ${CY}content-grade start${R} → HeadlineGrader compare${R}`);
|
|
743
783
|
blank();
|
|
744
784
|
if (!isProUser()) {
|
|
745
|
-
|
|
746
|
-
|
|
785
|
+
const usageCount = incrementUsage();
|
|
786
|
+
showFreeTierCTA(usageCount);
|
|
747
787
|
}
|
|
748
788
|
}
|
|
749
789
|
|
|
@@ -823,7 +863,7 @@ async function cmdInit() {
|
|
|
823
863
|
console.log(` ${CY}content-grade start${R}`);
|
|
824
864
|
blank();
|
|
825
865
|
}
|
|
826
|
-
console.log(` ${D}Pro tier:
|
|
866
|
+
console.log(` ${D}Pro tier: unlimited analyses/day + batch mode — ${CY}content-grade.onrender.com${R}`);
|
|
827
867
|
blank();
|
|
828
868
|
}
|
|
829
869
|
|
|
@@ -843,14 +883,14 @@ async function cmdActivate() {
|
|
|
843
883
|
blank();
|
|
844
884
|
console.log(` ${B}Pro features:${R}`);
|
|
845
885
|
console.log(` ${D} content-grade batch ./posts/ ${R}${D}# analyze all files in a directory${R}`);
|
|
846
|
-
console.log(` ${D}
|
|
886
|
+
console.log(` ${D} Unlimited analyses/day (vs 3 free)${R}`);
|
|
847
887
|
blank();
|
|
848
888
|
return;
|
|
849
889
|
}
|
|
850
890
|
|
|
851
891
|
console.log(` ${D}Pro tier unlocks:${R}`);
|
|
852
892
|
console.log(` ${D} · ${CY}content-grade batch <dir>${R}${D} — analyze entire directories${R}`);
|
|
853
|
-
console.log(` ${D} ·
|
|
893
|
+
console.log(` ${D} · Unlimited analyses/day (vs 3 free)${R}`);
|
|
854
894
|
blank();
|
|
855
895
|
console.log(` ${D}Get a license: ${CY}content-grade.onrender.com${R}${D} → Pricing${R}`);
|
|
856
896
|
blank();
|
|
@@ -899,6 +939,14 @@ async function cmdActivate() {
|
|
|
899
939
|
process.exit(1);
|
|
900
940
|
}
|
|
901
941
|
|
|
942
|
+
// Validate key format: CG-XXXX-XXXX-XXXX-XXXX
|
|
943
|
+
if (!isValidKeyFormat(key)) {
|
|
944
|
+
blank();
|
|
945
|
+
fail(`Invalid key format. Expected: CG-XXXX-XXXX-XXXX-XXXX (e.g. CG-A1B2-C3D4-E5F6-A7B8)`);
|
|
946
|
+
blank();
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
|
|
902
950
|
// Validate key against server before storing
|
|
903
951
|
const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://content-grade.onrender.com';
|
|
904
952
|
blank();
|
|
@@ -945,6 +993,14 @@ async function cmdActivate() {
|
|
|
945
993
|
config.activatedAt = new Date().toISOString();
|
|
946
994
|
saveConfig(config);
|
|
947
995
|
|
|
996
|
+
// Also write canonical license file to ~/.content-grade/license.json
|
|
997
|
+
mkdirSync(LICENSE_DIR, { recursive: true });
|
|
998
|
+
writeFileSync(LICENSE_FILE, JSON.stringify({
|
|
999
|
+
key,
|
|
1000
|
+
tier: activatedTier,
|
|
1001
|
+
activatedAt: config.activatedAt,
|
|
1002
|
+
}, null, 2), 'utf8');
|
|
1003
|
+
|
|
948
1004
|
const tierLimit = TIER_LIMITS[activatedTier] || TIER_LIMITS.free;
|
|
949
1005
|
blank();
|
|
950
1006
|
ok(`${(TIER_NAMES[activatedTier] || activatedTier).split(' —')[0]} activated! Your daily limit is now ${tierLimit} analyses.`);
|
|
@@ -1072,7 +1128,7 @@ async function cmdBatch(dirPath) {
|
|
|
1072
1128
|
'claude-haiku-4-5-20251001'
|
|
1073
1129
|
);
|
|
1074
1130
|
const r = parseJSON(raw);
|
|
1075
|
-
incrementUsage();
|
|
1131
|
+
if (!isProUser()) incrementUsage();
|
|
1076
1132
|
const sc = r.total_score;
|
|
1077
1133
|
const scoreColor = sc >= 70 ? GN : sc >= 45 ? YL : RD;
|
|
1078
1134
|
process.stdout.write(`\r ${scoreColor}${String(sc).padStart(3)}/100${R} ${gradeLetter(sc)} ${rel}${' '.repeat(10)}\n`);
|
|
@@ -1444,14 +1500,12 @@ function cmdHelp() {
|
|
|
1444
1500
|
console.log(` · Node.js 18+`);
|
|
1445
1501
|
blank();
|
|
1446
1502
|
|
|
1447
|
-
console.log(` ${B}
|
|
1503
|
+
console.log(` ${B}PRICING${R}`);
|
|
1448
1504
|
blank();
|
|
1449
|
-
console.log(`
|
|
1450
|
-
console.log(`
|
|
1451
|
-
console.log(`
|
|
1452
|
-
console.log(`
|
|
1453
|
-
console.log(` · Email subject line optimizer`);
|
|
1454
|
-
console.log(` · Audience archetype decoder`);
|
|
1505
|
+
console.log(` Free 3/day $0`);
|
|
1506
|
+
console.log(` Pro Unlimited $9/mo`);
|
|
1507
|
+
console.log(` Business 100/day $29/mo`);
|
|
1508
|
+
console.log(` Team 500/day $79/mo`);
|
|
1455
1509
|
blank();
|
|
1456
1510
|
console.log(` Purchase at: ${CY}content-grade.onrender.com${R}`);
|
|
1457
1511
|
console.log(` Then unlock: ${CY}content-grade activate <your-license-key>${R}`);
|
|
@@ -1622,10 +1676,9 @@ async function cmdDemo() {
|
|
|
1622
1676
|
console.log(` ${CY}npx content-grade headline "Your title here"${R} ${D}# grade a single headline${R}`);
|
|
1623
1677
|
blank();
|
|
1624
1678
|
|
|
1625
|
-
// Show telemetry notice
|
|
1679
|
+
// Show telemetry notice on first run (opt-out model)
|
|
1626
1680
|
if (_showTelemNotice) {
|
|
1627
|
-
console.log(` ${D}
|
|
1628
|
-
console.log(` ${D}No PII, no file contents. View what's collected: ${CY}content-grade telemetry${R}`);
|
|
1681
|
+
console.log(` ${D}Anonymous usage analytics enabled — no PII, no file contents. Opt out: ${CY}content-grade telemetry off${R}`);
|
|
1629
1682
|
blank();
|
|
1630
1683
|
}
|
|
1631
1684
|
|
|
@@ -1922,7 +1975,7 @@ function analysisToMarkdown(result, sourceName, analyzedAt) {
|
|
|
1922
1975
|
}
|
|
1923
1976
|
|
|
1924
1977
|
lines.push(`---`);
|
|
1925
|
-
lines.push(`Generated by [ContentGrade](https://
|
|
1978
|
+
lines.push(`Generated by [ContentGrade](https://content-grade.onrender.com) v${_version}`);
|
|
1926
1979
|
return lines.join('\n');
|
|
1927
1980
|
}
|
|
1928
1981
|
|
|
@@ -2071,7 +2124,7 @@ if (_rawArgs.includes('--help') || _rawArgs.includes('-h')) {
|
|
|
2071
2124
|
}
|
|
2072
2125
|
|
|
2073
2126
|
if (_demoMode) {
|
|
2074
|
-
recordEvent({ event: '
|
|
2127
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'demo' });
|
|
2075
2128
|
cmdDemo().catch(err => {
|
|
2076
2129
|
blank();
|
|
2077
2130
|
fail(`Demo error: ${err.message}`);
|
|
@@ -2083,7 +2136,7 @@ if (_demoMode) {
|
|
|
2083
2136
|
case 'analyse':
|
|
2084
2137
|
case 'check':
|
|
2085
2138
|
case 'grade':
|
|
2086
|
-
recordEvent({ event: '
|
|
2139
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze' });
|
|
2087
2140
|
cmdAnalyze(args[1]).catch(err => {
|
|
2088
2141
|
blank();
|
|
2089
2142
|
fail(`Unexpected error: ${err.message}`);
|
|
@@ -2093,7 +2146,7 @@ if (_demoMode) {
|
|
|
2093
2146
|
break;
|
|
2094
2147
|
|
|
2095
2148
|
case 'demo':
|
|
2096
|
-
recordEvent({ event: '
|
|
2149
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'demo' });
|
|
2097
2150
|
cmdDemo().catch(err => {
|
|
2098
2151
|
blank();
|
|
2099
2152
|
fail(`Demo error: ${err.message}`);
|
|
@@ -2103,7 +2156,7 @@ if (_demoMode) {
|
|
|
2103
2156
|
break;
|
|
2104
2157
|
|
|
2105
2158
|
case 'headline':
|
|
2106
|
-
recordEvent({ event: '
|
|
2159
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'headline' });
|
|
2107
2160
|
cmdHeadline(args.slice(1).join(' ')).catch(err => {
|
|
2108
2161
|
blank();
|
|
2109
2162
|
fail(`Unexpected error: ${err.message}`);
|
|
@@ -2114,13 +2167,13 @@ if (_demoMode) {
|
|
|
2114
2167
|
|
|
2115
2168
|
case 'start':
|
|
2116
2169
|
case 'serve':
|
|
2117
|
-
recordEvent({ event: '
|
|
2170
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'start' });
|
|
2118
2171
|
cmdStart();
|
|
2119
2172
|
break;
|
|
2120
2173
|
|
|
2121
2174
|
case 'init':
|
|
2122
2175
|
case 'setup':
|
|
2123
|
-
recordEvent({ event: '
|
|
2176
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'init' });
|
|
2124
2177
|
cmdInit().catch(err => {
|
|
2125
2178
|
blank();
|
|
2126
2179
|
fail(`Setup error: ${err.message}`);
|
|
@@ -2131,7 +2184,7 @@ if (_demoMode) {
|
|
|
2131
2184
|
|
|
2132
2185
|
case 'activate':
|
|
2133
2186
|
case 'license':
|
|
2134
|
-
recordEvent({ event: '
|
|
2187
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'activate' });
|
|
2135
2188
|
cmdActivate().catch(err => {
|
|
2136
2189
|
blank();
|
|
2137
2190
|
fail(`Activation error: ${err.message}`);
|
|
@@ -2142,7 +2195,7 @@ if (_demoMode) {
|
|
|
2142
2195
|
|
|
2143
2196
|
case 'seo-audit':
|
|
2144
2197
|
case 'seoaudit':
|
|
2145
|
-
recordEvent({ event: '
|
|
2198
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'seo-audit' });
|
|
2146
2199
|
if (!args[1]) {
|
|
2147
2200
|
blank();
|
|
2148
2201
|
fail(`No URL specified.`);
|
|
@@ -2161,7 +2214,7 @@ if (_demoMode) {
|
|
|
2161
2214
|
break;
|
|
2162
2215
|
|
|
2163
2216
|
case 'batch':
|
|
2164
|
-
recordEvent({ event: '
|
|
2217
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'batch' });
|
|
2165
2218
|
cmdBatch(args[1]).catch(err => {
|
|
2166
2219
|
blank();
|
|
2167
2220
|
fail(`Batch error: ${err.message}`);
|
|
@@ -2179,7 +2232,7 @@ if (_demoMode) {
|
|
|
2179
2232
|
case 'check-updates':
|
|
2180
2233
|
case 'update':
|
|
2181
2234
|
case 'upgrade-check':
|
|
2182
|
-
recordEvent({ event: '
|
|
2235
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'check-updates' });
|
|
2183
2236
|
cmdCheckUpdates().catch(err => {
|
|
2184
2237
|
blank();
|
|
2185
2238
|
fail(`Update check error: ${err.message}`);
|
|
@@ -2234,13 +2287,13 @@ if (_demoMode) {
|
|
|
2234
2287
|
|
|
2235
2288
|
case undefined:
|
|
2236
2289
|
// No command — show instant static demo (no Claude required, zero wait)
|
|
2237
|
-
recordEvent({ event: '
|
|
2290
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'quick_demo' });
|
|
2238
2291
|
cmdQuickDemo();
|
|
2239
2292
|
break;
|
|
2240
2293
|
|
|
2241
2294
|
default:
|
|
2242
2295
|
if (/^https?:\/\//i.test(raw)) {
|
|
2243
|
-
recordEvent({ event: '
|
|
2296
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze_url' });
|
|
2244
2297
|
cmdAnalyzeUrl(raw).catch(err => {
|
|
2245
2298
|
blank();
|
|
2246
2299
|
fail(`Unexpected error: ${err.message}`);
|
|
@@ -2274,7 +2327,7 @@ if (_demoMode) {
|
|
|
2274
2327
|
} catch {
|
|
2275
2328
|
target = raw; // let cmdAnalyze handle the "not found" error
|
|
2276
2329
|
}
|
|
2277
|
-
recordEvent({ event: '
|
|
2330
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'analyze' });
|
|
2278
2331
|
cmdAnalyze(target).catch(err => {
|
|
2279
2332
|
blank();
|
|
2280
2333
|
fail(`Unexpected error: ${err.message}`);
|
|
@@ -2287,7 +2340,7 @@ if (_demoMode) {
|
|
|
2287
2340
|
// npx content-grade Why most startups fail
|
|
2288
2341
|
const allText = args.join(' ').trim();
|
|
2289
2342
|
if (allText && allText.length >= 5 && !allText.startsWith('-')) {
|
|
2290
|
-
recordEvent({ event: '
|
|
2343
|
+
recordEvent({ event: 'grade_run', is_pro: isProUser(), command:'headline_smart' });
|
|
2291
2344
|
cmdHeadline(allText).catch(err => {
|
|
2292
2345
|
blank();
|
|
2293
2346
|
fail(`Unexpected error: ${err.message}`);
|