content-grade 1.0.3 → 1.0.5
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/README.md +53 -3
- package/bin/content-grade.js +163 -50
- package/bin/telemetry.js +3 -2
- package/dist/assets/index-DWBbWitG.js +78 -0
- package/dist/index.html +1 -1
- package/dist-server/server/app.js +4 -0
- package/dist-server/server/db.js +46 -0
- package/dist-server/server/routes/analytics.js +283 -0
- package/dist-server/server/routes/demos.js +9 -1
- package/dist-server/server/routes/license.js +53 -0
- package/dist-server/server/routes/stripe.js +69 -0
- package/dist-server/server/services/license.js +38 -0
- package/package.json +1 -1
- package/dist/assets/index-BUN69TiT.js +0 -78
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
8
|
[](https://claude.ai/code)
|
|
9
|
+
[](https://github.com/StanislavBG/Content-Grade/discussions)
|
|
10
|
+
[](EARLY_ADOPTERS.md)
|
|
9
11
|
|
|
10
12
|
---
|
|
11
13
|
|
|
@@ -82,7 +84,7 @@ npx content-grade activate
|
|
|
82
84
|
1. Why 90% of SaaS Startups Fail at Month 18 (and How to Be the 10%)
|
|
83
85
|
2. The Month 18 Startup Trap: What Kills Growth-Stage Companies
|
|
84
86
|
|
|
85
|
-
Unlock team features at
|
|
87
|
+
Unlock team features at content-grade.github.io/Content-Grade
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
---
|
|
@@ -139,7 +141,7 @@ Launch with `content-grade start` — opens at [http://localhost:4000](http://lo
|
|
|
139
141
|
| **EmailForge** | `/email-forge` | Subject line + body copy for click-through optimization |
|
|
140
142
|
| **AudienceDecoder** | `/audience` | Twitter handle → audience archetypes and content patterns |
|
|
141
143
|
|
|
142
|
-
Free tier: **50 analyses/day per tool**. Pro ($
|
|
144
|
+
Free tier: **50 analyses/day per tool**. Pro ($19/mo): **100 analyses/day** + all tools.
|
|
143
145
|
|
|
144
146
|
---
|
|
145
147
|
|
|
@@ -709,7 +711,7 @@ echo "$DATE,$HEADLINE,$SCORE" >> headline-scores.csv
|
|
|
709
711
|
|
|
710
712
|
| | Free | Pro |
|
|
711
713
|
|-|------|-----|
|
|
712
|
-
| Price | Free forever | $
|
|
714
|
+
| Price | Free forever | $19/month |
|
|
713
715
|
| Analyses/day (CLI) | Unlimited | Unlimited |
|
|
714
716
|
| Analyses/day (web dashboard, per tool) | 3 | 100 |
|
|
715
717
|
| HeadlineGrader (single grade) | ✓ | ✓ |
|
|
@@ -736,6 +738,54 @@ The web dashboard works the same way — every tool call goes through `claude -p
|
|
|
736
738
|
|
|
737
739
|
---
|
|
738
740
|
|
|
741
|
+
## Community
|
|
742
|
+
|
|
743
|
+
ContentGrade is built in public. The community is the roadmap.
|
|
744
|
+
|
|
745
|
+
### GitHub Discussions
|
|
746
|
+
|
|
747
|
+
**[Join the conversation →](https://github.com/StanislavBG/Content-Grade/discussions)**
|
|
748
|
+
|
|
749
|
+
| Category | What it's for |
|
|
750
|
+
|----------|--------------|
|
|
751
|
+
| [Q&A](https://github.com/StanislavBG/Content-Grade/discussions/new?category=q-a) | "Why is my score lower than expected?" — questions get answered here |
|
|
752
|
+
| [Show & Tell](https://github.com/StanislavBG/Content-Grade/discussions/new?category=show-and-tell) | Share your workflow, integration, or results — early adopters post here |
|
|
753
|
+
| [Ideas](https://github.com/StanislavBG/Content-Grade/discussions/new?category=ideas) | Feature requests and suggestions before they become issues |
|
|
754
|
+
|
|
755
|
+
### Early Adopter Program — 50 seats
|
|
756
|
+
|
|
757
|
+
**[EARLY_ADOPTERS.md](EARLY_ADOPTERS.md)** — The first 50 users who install and share feedback get:
|
|
758
|
+
|
|
759
|
+
- **Pro tier free for 12 months** ($19/mo value)
|
|
760
|
+
- Direct feedback channel with the maintainer
|
|
761
|
+
- Name in CONTRIBUTORS.md (permanent)
|
|
762
|
+
- Roadmap preview + design input before features ship
|
|
763
|
+
- 30-minute onboarding call (optional)
|
|
764
|
+
|
|
765
|
+
One action to claim: run ContentGrade, then post in [Show & Tell](https://github.com/StanislavBG/Content-Grade/discussions/new?category=show-and-tell) with `[Early Adopter]` in the title.
|
|
766
|
+
|
|
767
|
+
### Champions Program
|
|
768
|
+
|
|
769
|
+
Power users who go beyond early adoption get early access to `@beta` releases, a private Champions discussion thread, and a SVG badge for their profile. See **[CHAMPIONS.md](CHAMPIONS.md)** for selection criteria and current members.
|
|
770
|
+
|
|
771
|
+
### Contributing
|
|
772
|
+
|
|
773
|
+
See **[CONTRIBUTING.md](CONTRIBUTING.md)** for local dev setup, PR guidelines, and how to write good bug reports. The short version: clone → `pnpm install` → `pnpm dev` → file a PR. All contributions get a review within 48 hours.
|
|
774
|
+
|
|
775
|
+
### Roadmap
|
|
776
|
+
|
|
777
|
+
**[ROADMAP.md](ROADMAP.md)** — what's built, what's next, what's planned, and what we're deliberately not doing. Open a [Discussion](https://github.com/StanislavBG/Content-Grade/discussions/new?category=ideas) to challenge or extend it.
|
|
778
|
+
|
|
779
|
+
### Feedback Loop
|
|
780
|
+
|
|
781
|
+
**[FEEDBACK_LOOP.md](FEEDBACK_LOOP.md)** — how feedback flows into product decisions. Monthly structured check-ins, 48h triage SLA, and roadmap sync. Use the [monthly feedback template](https://github.com/StanislavBG/Content-Grade/issues/new?template=monthly_feedback.yml) to share your experience.
|
|
782
|
+
|
|
783
|
+
### Contributors
|
|
784
|
+
|
|
785
|
+
**[CONTRIBUTORS.md](CONTRIBUTORS.md)** — everyone who has improved ContentGrade. Code, bug reports, and real-world feedback all count. Early Adopters and Champions are listed here permanently.
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
739
789
|
## License
|
|
740
790
|
|
|
741
791
|
MIT
|
package/bin/content-grade.js
CHANGED
|
@@ -713,34 +713,42 @@ async function cmdActivate() {
|
|
|
713
713
|
console.log(` ${D}Get a license: ${CY}contentgrade.dev${R}${D} → Pricing → Team${R}`);
|
|
714
714
|
blank();
|
|
715
715
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
process.
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
716
|
+
let key;
|
|
717
|
+
if (args[1] && args[1].length >= 8) {
|
|
718
|
+
key = args[1];
|
|
719
|
+
blank();
|
|
720
|
+
console.log(` ${D}Using provided key: ${args[1].slice(0, 8)}...${R}`);
|
|
721
|
+
blank();
|
|
722
|
+
} else {
|
|
723
|
+
process.stdout.write(` ${CY}License key:${R} `);
|
|
724
|
+
|
|
725
|
+
key = await new Promise(res => {
|
|
726
|
+
let input = '';
|
|
727
|
+
const isRaw = process.stdin.isTTY;
|
|
728
|
+
if (isRaw) process.stdin.setRawMode(true);
|
|
729
|
+
process.stdin.resume();
|
|
730
|
+
process.stdin.setEncoding('utf8');
|
|
731
|
+
process.stdin.on('data', function onData(chunk) {
|
|
732
|
+
if (chunk === '\r' || chunk === '\n') {
|
|
733
|
+
if (isRaw) process.stdin.setRawMode(false);
|
|
734
|
+
process.stdin.pause();
|
|
735
|
+
process.stdin.removeListener('data', onData);
|
|
736
|
+
process.stdout.write('\n');
|
|
737
|
+
res(input.trim());
|
|
738
|
+
} else if (chunk === '\u0003') {
|
|
739
|
+
if (isRaw) process.stdin.setRawMode(false);
|
|
740
|
+
process.stdin.pause();
|
|
741
|
+
process.stdout.write('\n');
|
|
742
|
+
process.exit(0);
|
|
743
|
+
} else if (chunk === '\u007f') {
|
|
744
|
+
if (input.length > 0) { input = input.slice(0, -1); process.stdout.write('\b \b'); }
|
|
745
|
+
} else {
|
|
746
|
+
input += chunk;
|
|
747
|
+
process.stdout.write('*');
|
|
748
|
+
}
|
|
749
|
+
});
|
|
742
750
|
});
|
|
743
|
-
}
|
|
751
|
+
}
|
|
744
752
|
|
|
745
753
|
if (!key || key.length < 8) {
|
|
746
754
|
blank();
|
|
@@ -749,13 +757,45 @@ async function cmdActivate() {
|
|
|
749
757
|
process.exit(2);
|
|
750
758
|
}
|
|
751
759
|
|
|
760
|
+
// Validate key against server before storing
|
|
761
|
+
const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://contentgrade.ai';
|
|
762
|
+
blank();
|
|
763
|
+
process.stdout.write(` ${D}Validating key...${R}`);
|
|
764
|
+
|
|
765
|
+
let validated = false;
|
|
766
|
+
try {
|
|
767
|
+
const response = await fetch(`${serverUrl}/api/license/validate`, {
|
|
768
|
+
method: 'POST',
|
|
769
|
+
headers: { 'Content-Type': 'application/json' },
|
|
770
|
+
body: JSON.stringify({ key }),
|
|
771
|
+
});
|
|
772
|
+
const data = await response.json();
|
|
773
|
+
if (data.valid) {
|
|
774
|
+
validated = true;
|
|
775
|
+
} else {
|
|
776
|
+
process.stdout.write('\n');
|
|
777
|
+
blank();
|
|
778
|
+
fail(data.message || 'Invalid or expired license key. Visit contentgrade.ai to check your subscription.');
|
|
779
|
+
blank();
|
|
780
|
+
process.exit(2);
|
|
781
|
+
}
|
|
782
|
+
} catch {
|
|
783
|
+
// Server unreachable — allow offline activation with a warning
|
|
784
|
+
process.stdout.write('\n');
|
|
785
|
+
blank();
|
|
786
|
+
console.log(` ${D}⚠ Could not reach server — storing key locally without verification.${R}`);
|
|
787
|
+
validated = true;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
process.stdout.write(' done\n');
|
|
791
|
+
|
|
752
792
|
const config = loadConfig();
|
|
753
793
|
config.licenseKey = key;
|
|
754
794
|
config.activatedAt = new Date().toISOString();
|
|
755
795
|
saveConfig(config);
|
|
756
796
|
|
|
757
797
|
blank();
|
|
758
|
-
ok(`
|
|
798
|
+
ok(`Pro access activated! Your daily limit is now 100 analyses.`);
|
|
759
799
|
blank();
|
|
760
800
|
console.log(` ${B}Try it:${R}`);
|
|
761
801
|
console.log(` ${CY} content-grade batch ./posts/${R} ${D}# analyze all files${R}`);
|
|
@@ -1094,19 +1134,19 @@ function cmdHelp() {
|
|
|
1094
1134
|
|
|
1095
1135
|
// ── Demo command ──────────────────────────────────────────────────────────────
|
|
1096
1136
|
|
|
1097
|
-
const DEMO_CONTENT = `#
|
|
1137
|
+
const DEMO_CONTENT = `# How to Write Better Blog Posts
|
|
1098
1138
|
|
|
1099
|
-
|
|
1139
|
+
Writing good blog posts is important for any content marketer. Many people struggle with this. In this post I will share some tips that can help you improve.
|
|
1100
1140
|
|
|
1101
|
-
First,
|
|
1141
|
+
First, you need to have a good headline. The headline is the first thing people see. A good headline will make people want to read more. Try to make it interesting.
|
|
1102
1142
|
|
|
1103
|
-
Second,
|
|
1143
|
+
Second, write good content. Your content should be useful and informative. People should learn something from reading your post. Make sure to cover the topic well.
|
|
1104
1144
|
|
|
1105
|
-
Third,
|
|
1145
|
+
Third, use a good structure. Break your content into sections. Use headings and bullet points. This makes it easier to read. People can scan and find what they need.
|
|
1106
1146
|
|
|
1107
|
-
|
|
1147
|
+
Finally, have a good conclusion. Summarize what you talked about. Tell people what to do next. A call to action is important.
|
|
1108
1148
|
|
|
1109
|
-
In conclusion, these
|
|
1149
|
+
In conclusion, following these tips will help you write better blog posts. Good content is important for success online. Start applying these tips today.
|
|
1110
1150
|
`;
|
|
1111
1151
|
|
|
1112
1152
|
async function cmdDemo() {
|
|
@@ -1125,22 +1165,80 @@ async function cmdDemo() {
|
|
|
1125
1165
|
const tmpFile = `/tmp/content-grade-demo-${process.pid}-${Date.now()}.md`;
|
|
1126
1166
|
|
|
1127
1167
|
banner();
|
|
1128
|
-
console.log(` ${B}Demo Mode${R} ${D}—
|
|
1168
|
+
console.log(` ${B}Demo Mode${R} ${D}— see ContentGrade in action${R}`);
|
|
1129
1169
|
blank();
|
|
1130
|
-
console.log(` ${D}
|
|
1131
|
-
console.log(` ${
|
|
1132
|
-
blank();
|
|
1133
|
-
console.log(` ${D}Analyzing sample blog post...${R}`);
|
|
1170
|
+
console.log(` ${D}Analyzing a sample blog post with generic writing.${R}`);
|
|
1171
|
+
console.log(` ${D}Generic content scores low — that's the point. Watch what ContentGrade finds.${R}`);
|
|
1134
1172
|
blank();
|
|
1135
1173
|
|
|
1136
1174
|
writeFileSync(tmpFile, DEMO_CONTENT, 'utf8');
|
|
1137
1175
|
try {
|
|
1138
1176
|
await cmdAnalyze(tmpFile);
|
|
1177
|
+
|
|
1178
|
+
// Post-demo CTA — shown AFTER user has seen the analysis output
|
|
1179
|
+
blank();
|
|
1180
|
+
hr();
|
|
1181
|
+
console.log(` ${B}${CY}Now try it on your own content:${R}`);
|
|
1182
|
+
blank();
|
|
1183
|
+
console.log(` ${CY}content-grade analyze ./your-post.md${R} ${D}# any .md or .txt file${R}`);
|
|
1184
|
+
console.log(` ${CY}content-grade https://your-blog.com/post${R} ${D}# analyze any URL${R}`);
|
|
1185
|
+
console.log(` ${CY}content-grade headline "Your headline here"${R} ${D}# grade a headline${R}`);
|
|
1186
|
+
blank();
|
|
1187
|
+
|
|
1139
1188
|
// Show telemetry notice after user has seen value (first run only)
|
|
1140
1189
|
if (_showTelemNotice) {
|
|
1141
1190
|
console.log(` ${D}Anonymous usage data is collected by default. Opt out: ${CY}content-grade telemetry off${R}`);
|
|
1142
1191
|
blank();
|
|
1143
1192
|
}
|
|
1193
|
+
|
|
1194
|
+
// Interactive follow-up — strike while the iron is hot
|
|
1195
|
+
if (process.stdin.isTTY) {
|
|
1196
|
+
hr();
|
|
1197
|
+
console.log(` ${B}${CY}Try it on YOUR content right now:${R}`);
|
|
1198
|
+
blank();
|
|
1199
|
+
console.log(` ${D}Paste a URL, type a headline, or a file path — then press Enter.${R}`);
|
|
1200
|
+
console.log(` ${D}(Press Enter to skip)${R}`);
|
|
1201
|
+
blank();
|
|
1202
|
+
process.stdout.write(` ${CY}>${R} `);
|
|
1203
|
+
|
|
1204
|
+
const userInput = await new Promise(res => {
|
|
1205
|
+
let line = '';
|
|
1206
|
+
process.stdin.resume();
|
|
1207
|
+
process.stdin.setEncoding('utf8');
|
|
1208
|
+
const onData = (chunk) => {
|
|
1209
|
+
const str = chunk.toString();
|
|
1210
|
+
if (str.includes('\n') || str.includes('\r')) {
|
|
1211
|
+
process.stdin.pause();
|
|
1212
|
+
process.stdin.removeListener('data', onData);
|
|
1213
|
+
process.stdout.write('\n');
|
|
1214
|
+
res(line.replace(/[\r\n]/g, '').trim());
|
|
1215
|
+
} else if (str === '\u0003') {
|
|
1216
|
+
process.stdin.pause();
|
|
1217
|
+
process.stdin.removeListener('data', onData);
|
|
1218
|
+
process.stdout.write('\n');
|
|
1219
|
+
res('');
|
|
1220
|
+
} else {
|
|
1221
|
+
line += str;
|
|
1222
|
+
process.stdout.write(str);
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
process.stdin.on('data', onData);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
if (userInput) {
|
|
1229
|
+
blank();
|
|
1230
|
+
if (/^https?:\/\//i.test(userInput)) {
|
|
1231
|
+
await cmdAnalyzeUrl(userInput);
|
|
1232
|
+
} else if (looksLikePath(userInput)) {
|
|
1233
|
+
await cmdAnalyze(userInput);
|
|
1234
|
+
} else if (userInput.length >= 5) {
|
|
1235
|
+
await cmdHeadline(userInput);
|
|
1236
|
+
} else {
|
|
1237
|
+
warn(`Couldn't parse that. Try a URL like https://... or a headline like "Your title here".`);
|
|
1238
|
+
blank();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1144
1242
|
} finally {
|
|
1145
1243
|
try { unlinkSync(tmpFile); } catch {}
|
|
1146
1244
|
}
|
|
@@ -1483,15 +1581,30 @@ switch (cmd) {
|
|
|
1483
1581
|
process.exit(1);
|
|
1484
1582
|
});
|
|
1485
1583
|
} else {
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1584
|
+
// Smart fallback: treat unrecognised input as a headline to grade.
|
|
1585
|
+
// Covers: npx content-grade "Why most startups fail"
|
|
1586
|
+
// npx content-grade Why most startups fail
|
|
1587
|
+
const allText = args.join(' ').trim();
|
|
1588
|
+
if (allText && allText.length >= 5 && !allText.startsWith('-')) {
|
|
1589
|
+
recordEvent({ event: 'command', command: 'headline_smart' });
|
|
1590
|
+
cmdHeadline(allText).catch(err => {
|
|
1591
|
+
blank();
|
|
1592
|
+
fail(`Unexpected error: ${err.message}`);
|
|
1593
|
+
blank();
|
|
1594
|
+
process.exit(1);
|
|
1595
|
+
});
|
|
1596
|
+
} else {
|
|
1597
|
+
blank();
|
|
1598
|
+
fail(`Unknown command: ${B}${raw}${R}`);
|
|
1599
|
+
blank();
|
|
1600
|
+
console.log(` Available commands:`);
|
|
1601
|
+
console.log(` ${CY}analyze <file>${R} ${CY}headline "<text>"${R} ${CY}demo${R} ${CY}start${R} ${CY}init${R} ${CY}help${R}`);
|
|
1602
|
+
blank();
|
|
1603
|
+
console.log(` ${D}Pass a file/directory directly: ${CY}content-grade ./my-post.md${R}`);
|
|
1604
|
+
console.log(` ${D}Grade a headline: ${CY}content-grade "Your headline text"${R}`);
|
|
1605
|
+
console.log(` ${D}Run ${CY}content-grade help${R}${D} for full usage${R}`);
|
|
1606
|
+
blank();
|
|
1607
|
+
process.exit(1);
|
|
1608
|
+
}
|
|
1496
1609
|
}
|
|
1497
1610
|
}
|
package/bin/telemetry.js
CHANGED
|
@@ -17,8 +17,9 @@ const CONFIG_DIR = join(homedir(), '.content-grade');
|
|
|
17
17
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
18
18
|
const EVENTS_FILE = join(CONFIG_DIR, 'events.jsonl');
|
|
19
19
|
|
|
20
|
-
// Telemetry endpoint:
|
|
21
|
-
|
|
20
|
+
// Telemetry endpoint: CLI events are forwarded here when telemetry is enabled.
|
|
21
|
+
// Override with CONTENT_GRADE_TELEMETRY_URL env var (e.g. for self-hosted or staging).
|
|
22
|
+
const REMOTE_URL = process.env.CONTENT_GRADE_TELEMETRY_URL || 'https://contentgrade.ai/api/telemetry';
|
|
22
23
|
|
|
23
24
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
24
25
|
|