content-grade 1.0.1 → 1.0.2
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 +9 -9
- package/bin/content-grade.js +152 -35
- package/dist-server/server/routes/demos.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,10 @@ npx content-grade
|
|
|
21
21
|
# 3. Analyze your own content
|
|
22
22
|
npx content-grade ./my-post.md
|
|
23
23
|
|
|
24
|
-
# 4.
|
|
24
|
+
# 4. Analyze a live URL directly
|
|
25
|
+
npx content-grade https://yoursite.com/blog/post
|
|
26
|
+
|
|
27
|
+
# 5. Grade a single headline
|
|
25
28
|
npx content-grade headline "Why Most Startups Fail at Month 18"
|
|
26
29
|
```
|
|
27
30
|
|
|
@@ -72,11 +75,7 @@ npx content-grade headline "Why Most Startups Fail at Month 18"
|
|
|
72
75
|
1. Why 90% of SaaS Startups Fail at Month 18 (and How to Be the 10%)
|
|
73
76
|
2. The Month 18 Startup Trap: What Kills Growth-Stage Companies
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
· Competitor headline comparison
|
|
77
|
-
· Landing page URL audit (full CRO analysis)
|
|
78
|
-
· Ad copy scoring (Google, Meta, LinkedIn)
|
|
79
|
-
· 100 analyses/day vs 3 free
|
|
78
|
+
Unlock team features at contentgrade.dev
|
|
80
79
|
```
|
|
81
80
|
|
|
82
81
|
---
|
|
@@ -86,6 +85,7 @@ npx content-grade headline "Why Most Startups Fail at Month 18"
|
|
|
86
85
|
| Command | What it does |
|
|
87
86
|
|---------|-------------|
|
|
88
87
|
| *(no args)* | Instant demo on built-in sample content |
|
|
88
|
+
| `<url>` | Fetch and analyze a live URL — `npx content-grade https://example.com/blog/post` |
|
|
89
89
|
| `<file>` | Analyze a file directly — `npx content-grade ./post.md` |
|
|
90
90
|
| `.` or `<dir>` | Scan a directory, find and analyze the best content file |
|
|
91
91
|
| `demo` | Same as no args |
|
|
@@ -122,7 +122,7 @@ Launch with `content-grade start` — opens at [http://localhost:4000](http://lo
|
|
|
122
122
|
| **EmailForge** | `/email-forge` | Subject line + body copy for click-through optimization |
|
|
123
123
|
| **AudienceDecoder** | `/audience` | Twitter handle → audience archetypes and content patterns |
|
|
124
124
|
|
|
125
|
-
Free tier: **
|
|
125
|
+
Free tier: **50 analyses/day per tool**. Pro ($9/mo): **100 analyses/day** + all tools.
|
|
126
126
|
|
|
127
127
|
---
|
|
128
128
|
|
|
@@ -537,8 +537,8 @@ curl -s -X POST http://localhost:4000/api/demos/email-forge \
|
|
|
537
537
|
"gated": true,
|
|
538
538
|
"isPro": false,
|
|
539
539
|
"remaining": 0,
|
|
540
|
-
"limit":
|
|
541
|
-
"message": "Free daily limit reached (
|
|
540
|
+
"limit": 50,
|
|
541
|
+
"message": "Free daily limit reached (50/day). Get 100 analyses/day with Pro"
|
|
542
542
|
}
|
|
543
543
|
```
|
|
544
544
|
|
package/bin/content-grade.js
CHANGED
|
@@ -18,6 +18,8 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, statSyn
|
|
|
18
18
|
import { resolve, dirname, basename, extname } from 'path';
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
20
|
import { promisify } from 'util';
|
|
21
|
+
import { get as httpsGet } from 'https';
|
|
22
|
+
import { get as httpGet } from 'http';
|
|
21
23
|
import { initTelemetry, recordEvent, disableTelemetry, enableTelemetry, telemetryStatus, isOptedOut } from './telemetry.js';
|
|
22
24
|
|
|
23
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -187,7 +189,7 @@ async function cmdAnalyze(filePath) {
|
|
|
187
189
|
blank();
|
|
188
190
|
console.log(` ${D}No file? Try the demo: ${CY}content-grade demo${R}`);
|
|
189
191
|
blank();
|
|
190
|
-
process.exit(
|
|
192
|
+
process.exit(2);
|
|
191
193
|
}
|
|
192
194
|
|
|
193
195
|
const absPath = resolve(process.cwd(), filePath);
|
|
@@ -198,7 +200,7 @@ async function cmdAnalyze(filePath) {
|
|
|
198
200
|
console.log(` ${YL}Check the path and try again.${R}`);
|
|
199
201
|
console.log(` ${D}Tip: use a relative path like ./my-file.md or an absolute path.${R}`);
|
|
200
202
|
blank();
|
|
201
|
-
process.exit(
|
|
203
|
+
process.exit(2);
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
// Guard: reject directories
|
|
@@ -209,7 +211,7 @@ async function cmdAnalyze(filePath) {
|
|
|
209
211
|
blank();
|
|
210
212
|
console.log(` ${D}Pass a text file (.md, .txt) — not a directory.${R}`);
|
|
211
213
|
blank();
|
|
212
|
-
process.exit(
|
|
214
|
+
process.exit(2);
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
// Guard: reject files over 500 KB before reading into memory
|
|
@@ -220,7 +222,7 @@ async function cmdAnalyze(filePath) {
|
|
|
220
222
|
blank();
|
|
221
223
|
console.log(` ${YL}Tip:${R} Copy the relevant section into a new file and analyze that.`);
|
|
222
224
|
blank();
|
|
223
|
-
process.exit(
|
|
225
|
+
process.exit(2);
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
const content = readFileSync(absPath, 'utf8');
|
|
@@ -233,20 +235,22 @@ async function cmdAnalyze(filePath) {
|
|
|
233
235
|
console.log(` ${YL}ContentGrade analyzes written content — blog posts, emails, ad copy, landing pages.${R}`);
|
|
234
236
|
console.log(` ${D}Supported formats: .md, .txt, .mdx, or any plain-text file.${R}`);
|
|
235
237
|
blank();
|
|
236
|
-
process.exit(
|
|
238
|
+
process.exit(2);
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
if (content.trim().length < 20) {
|
|
240
242
|
blank();
|
|
241
243
|
fail(`File is too short to analyze (${content.trim().length} chars). Add some content and try again.`);
|
|
242
244
|
blank();
|
|
243
|
-
process.exit(
|
|
245
|
+
process.exit(2);
|
|
244
246
|
}
|
|
245
247
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
if (!_jsonMode && !_quietMode) {
|
|
249
|
+
banner();
|
|
250
|
+
console.log(` ${B}Analyzing:${R} ${CY}${basename(absPath)}${R}`);
|
|
251
|
+
console.log(` ${D}${content.length.toLocaleString()} characters · detecting content type...${R}`);
|
|
252
|
+
blank();
|
|
253
|
+
}
|
|
250
254
|
|
|
251
255
|
// Check Claude
|
|
252
256
|
if (!checkClaude()) {
|
|
@@ -271,7 +275,7 @@ async function cmdAnalyze(filePath) {
|
|
|
271
275
|
|
|
272
276
|
const truncated = content.length > 6000 ? content.slice(0, 6000) + '\n\n[Content truncated for analysis]' : content;
|
|
273
277
|
|
|
274
|
-
process.stdout.write(` ${D}Running analysis (this takes ~15 seconds)...${R}`);
|
|
278
|
+
if (!_jsonMode && !_quietMode) process.stdout.write(` ${D}Running analysis (this takes ~15 seconds)...${R}`);
|
|
275
279
|
|
|
276
280
|
let result;
|
|
277
281
|
try {
|
|
@@ -280,9 +284,12 @@ async function cmdAnalyze(filePath) {
|
|
|
280
284
|
ANALYZE_SYSTEM,
|
|
281
285
|
'claude-sonnet-4-6'
|
|
282
286
|
);
|
|
283
|
-
process.stdout.write(`\r ${GN}✓${R} Analysis complete${' '.repeat(30)}\n`);
|
|
287
|
+
if (!_jsonMode && !_quietMode) process.stdout.write(`\r ${GN}✓${R} Analysis complete${' '.repeat(30)}\n`);
|
|
284
288
|
result = parseJSON(raw);
|
|
285
289
|
recordEvent({ event: 'analyze_result', score: result.total_score, content_type: result.content_type });
|
|
290
|
+
// Machine-readable output modes (exit cleanly, skip styled output)
|
|
291
|
+
if (_jsonMode) { process.stdout.write(JSON.stringify(result, null, 2) + '\n'); return; }
|
|
292
|
+
if (_quietMode) { process.stdout.write(`${result.total_score}\n`); return; }
|
|
286
293
|
} catch (err) {
|
|
287
294
|
process.stdout.write(`\n`);
|
|
288
295
|
blank();
|
|
@@ -373,18 +380,9 @@ async function cmdAnalyze(filePath) {
|
|
|
373
380
|
blank();
|
|
374
381
|
}
|
|
375
382
|
|
|
376
|
-
// ──
|
|
377
|
-
hr();
|
|
378
|
-
console.log(` ${D}Free analysis complete. ${MG}Pro unlocks:${R}`);
|
|
379
|
-
console.log(` ${D} · Competitor headline comparison${R}`);
|
|
380
|
-
console.log(` ${D} · Landing page URL audit (full CRO analysis)${R}`);
|
|
381
|
-
console.log(` ${D} · Ad copy scoring (Google, Meta, LinkedIn)${R}`);
|
|
382
|
-
console.log(` ${D} · Twitter thread grader with shareability score${R}`);
|
|
383
|
-
console.log(` ${D} · Email subject line optimizer${R}`);
|
|
384
|
-
console.log(` ${D} · 100 analyses/day vs 3 free${R}`);
|
|
383
|
+
// ── Subtle upsell
|
|
385
384
|
blank();
|
|
386
|
-
console.log(` ${D}
|
|
387
|
-
console.log(` ${D}Grade a headline: ${CY}content-grade headline "Your text here"${R}`);
|
|
385
|
+
console.log(` ${D}Unlock team features at ${CY}contentgrade.dev${R}`);
|
|
388
386
|
blank();
|
|
389
387
|
}
|
|
390
388
|
|
|
@@ -579,7 +577,7 @@ async function cmdInit() {
|
|
|
579
577
|
console.log(` ${CY}content-grade start${R}`);
|
|
580
578
|
blank();
|
|
581
579
|
}
|
|
582
|
-
console.log(` ${D}Pro tier ($9/mo): 100 analyses/day + competitor comparison + URL audits${R}`);
|
|
580
|
+
console.log(` ${D}Pro tier ($9/mo): 100 analyses/day (vs 50 free) + competitor comparison + URL audits${R}`);
|
|
583
581
|
blank();
|
|
584
582
|
}
|
|
585
583
|
|
|
@@ -657,7 +655,7 @@ function cmdStart() {
|
|
|
657
655
|
info(` EmailForge — ${url}/email-forge`);
|
|
658
656
|
info(` AudienceDecoder — ${url}/audience`);
|
|
659
657
|
blank();
|
|
660
|
-
info(`Free tier:
|
|
658
|
+
info(`Free tier: 50 analyses/day. Upgrade at ${url}`);
|
|
661
659
|
info(`Press Ctrl+C to stop`);
|
|
662
660
|
blank();
|
|
663
661
|
openBrowser(url);
|
|
@@ -758,7 +756,7 @@ function cmdHelp() {
|
|
|
758
756
|
|
|
759
757
|
console.log(` ${B}PRO TIER${R} ${MG}$9/month${R}`);
|
|
760
758
|
blank();
|
|
761
|
-
console.log(` · 100 analyses/day (vs
|
|
759
|
+
console.log(` · 100 analyses/day (vs 50 free)`);
|
|
762
760
|
console.log(` · Competitor headline A/B comparison`);
|
|
763
761
|
console.log(` · Landing page URL audit`);
|
|
764
762
|
console.log(` · Ad copy scoring (Google, Meta, LinkedIn)`);
|
|
@@ -787,6 +785,18 @@ In conclusion, these productivity tips are very helpful. Try them today and you
|
|
|
787
785
|
`;
|
|
788
786
|
|
|
789
787
|
async function cmdDemo() {
|
|
788
|
+
// If Claude isn't installed, skip the demo and run guided setup instead
|
|
789
|
+
if (!checkClaude()) {
|
|
790
|
+
console.log('');
|
|
791
|
+
console.log(` ${B}${CY}Welcome to ContentGrade${R}`);
|
|
792
|
+
console.log('');
|
|
793
|
+
console.log(` ${D}ContentGrade needs Claude CLI to run analysis.${R}`);
|
|
794
|
+
console.log(` ${D}Let's get you set up — it only takes a minute.${R}`);
|
|
795
|
+
console.log('');
|
|
796
|
+
await cmdInit();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
790
800
|
const tmpFile = `/tmp/content-grade-demo-${process.pid}-${Date.now()}.md`;
|
|
791
801
|
|
|
792
802
|
banner();
|
|
@@ -799,6 +809,107 @@ async function cmdDemo() {
|
|
|
799
809
|
blank();
|
|
800
810
|
|
|
801
811
|
writeFileSync(tmpFile, DEMO_CONTENT, 'utf8');
|
|
812
|
+
try {
|
|
813
|
+
await cmdAnalyze(tmpFile);
|
|
814
|
+
// Show telemetry notice after user has seen value (first run only)
|
|
815
|
+
if (_showTelemNotice) {
|
|
816
|
+
console.log(` ${D}Anonymous usage data is collected by default. Opt out: ${CY}content-grade telemetry off${R}`);
|
|
817
|
+
blank();
|
|
818
|
+
}
|
|
819
|
+
} finally {
|
|
820
|
+
try { unlinkSync(tmpFile); } catch {}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ── URL fetcher ───────────────────────────────────────────────────────────────
|
|
825
|
+
|
|
826
|
+
function fetchUrl(url) {
|
|
827
|
+
return new Promise((resolve, reject) => {
|
|
828
|
+
const get = url.startsWith('https') ? httpsGet : httpGet;
|
|
829
|
+
const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/StanislavBG/Content-Grade)' }, timeout: 15000 }, (res) => {
|
|
830
|
+
// Follow one redirect
|
|
831
|
+
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
|
|
832
|
+
fetchUrl(res.headers.location).then(resolve).catch(reject);
|
|
833
|
+
res.resume();
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
837
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
838
|
+
res.resume();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const chunks = [];
|
|
842
|
+
res.on('data', c => chunks.push(c));
|
|
843
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8', 0, 500000)));
|
|
844
|
+
res.on('error', reject);
|
|
845
|
+
});
|
|
846
|
+
req.on('error', reject);
|
|
847
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function htmlToText(html) {
|
|
852
|
+
return html
|
|
853
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
854
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
855
|
+
.replace(/<nav[\s\S]*?<\/nav>/gi, ' ')
|
|
856
|
+
.replace(/<footer[\s\S]*?<\/footer>/gi, ' ')
|
|
857
|
+
.replace(/<header[\s\S]*?<\/header>/gi, ' ')
|
|
858
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
859
|
+
.replace(/<(h[1-6])[^>]*>([\s\S]*?)<\/\1>/gi, (_, _t, txt) => `\n\n## ${txt}\n\n`)
|
|
860
|
+
.replace(/<(p|li|blockquote)[^>]*>([\s\S]*?)<\/\1>/gi, (_, _t, txt) => `\n${txt}\n`)
|
|
861
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
862
|
+
.replace(/<[^>]+>/g, ' ')
|
|
863
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ')
|
|
864
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
865
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
866
|
+
.trim();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async function cmdAnalyzeUrl(url) {
|
|
870
|
+
banner();
|
|
871
|
+
console.log(` ${B}Analyzing URL:${R} ${CY}${url}${R}`);
|
|
872
|
+
blank();
|
|
873
|
+
|
|
874
|
+
if (!checkClaude()) {
|
|
875
|
+
fail(`Claude CLI not found.`);
|
|
876
|
+
blank();
|
|
877
|
+
console.log(` ContentGrade uses your local Claude CLI — no API keys needed.`);
|
|
878
|
+
console.log(` Install from ${CY}https://claude.ai/code${R} then run ${CY}claude login${R}`);
|
|
879
|
+
blank();
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
process.stdout.write(` ${D}Fetching page...${R}`);
|
|
884
|
+
let html;
|
|
885
|
+
try {
|
|
886
|
+
html = await fetchUrl(url);
|
|
887
|
+
process.stdout.write(`\r ${GN}✓${R} Page fetched${' '.repeat(20)}\n`);
|
|
888
|
+
} catch (err) {
|
|
889
|
+
process.stdout.write(`\n`);
|
|
890
|
+
blank();
|
|
891
|
+
fail(`Could not fetch URL: ${err.message}`);
|
|
892
|
+
blank();
|
|
893
|
+
console.log(` ${YL}Check:${R}`);
|
|
894
|
+
console.log(` ${D}· URL is accessible (try opening it in a browser)${R}`);
|
|
895
|
+
console.log(` ${D}· You have internet access${R}`);
|
|
896
|
+
blank();
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const text = htmlToText(html);
|
|
901
|
+
if (text.length < 50) {
|
|
902
|
+
blank();
|
|
903
|
+
fail(`Could not extract readable content from ${url}`);
|
|
904
|
+
blank();
|
|
905
|
+
console.log(` ${D}The page may be JavaScript-rendered. Try saving the page content to a .md file and running:${R}`);
|
|
906
|
+
console.log(` ${CY} content-grade analyze ./page.md${R}`);
|
|
907
|
+
blank();
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const tmpFile = `/tmp/content-grade-url-${process.pid}-${Date.now()}.md`;
|
|
912
|
+
writeFileSync(tmpFile, `# ${url}\n\n${text}`, 'utf8');
|
|
802
913
|
try {
|
|
803
914
|
await cmdAnalyze(tmpFile);
|
|
804
915
|
} finally {
|
|
@@ -853,19 +964,17 @@ function findBestContentFile(dirPath) {
|
|
|
853
964
|
|
|
854
965
|
// ── Router ────────────────────────────────────────────────────────────────────
|
|
855
966
|
|
|
856
|
-
const
|
|
967
|
+
const _rawArgs = process.argv.slice(2);
|
|
968
|
+
const _jsonMode = _rawArgs.includes('--json');
|
|
969
|
+
const _quietMode = _rawArgs.includes('--quiet');
|
|
970
|
+
const args = _rawArgs.filter(a => a !== '--no-telemetry' && a !== '--json' && a !== '--quiet');
|
|
857
971
|
const raw = args[0];
|
|
858
972
|
const cmd = raw?.toLowerCase();
|
|
859
973
|
|
|
860
974
|
// ── Telemetry init ────────────────────────────────────────────────────────────
|
|
861
975
|
const _telem = initTelemetry();
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
blank();
|
|
865
|
-
console.log(` ${D}ContentGrade collects anonymous usage data (command name, score, duration).${R}`);
|
|
866
|
-
console.log(` ${D}No file contents, no PII. To opt out: ${CY}content-grade telemetry off${R}${D} or pass ${CY}--no-telemetry${R}`);
|
|
867
|
-
blank();
|
|
868
|
-
}
|
|
976
|
+
// Defer first-run telemetry notice so users see value first
|
|
977
|
+
const _showTelemNotice = _telem.isNew;
|
|
869
978
|
|
|
870
979
|
// Smart path detection: first arg is a path if it starts with ., /, ~ or is an existing FS entry
|
|
871
980
|
function looksLikePath(s) {
|
|
@@ -984,7 +1093,15 @@ switch (cmd) {
|
|
|
984
1093
|
break;
|
|
985
1094
|
|
|
986
1095
|
default:
|
|
987
|
-
if (
|
|
1096
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
1097
|
+
recordEvent({ event: 'command', command: 'analyze_url' });
|
|
1098
|
+
cmdAnalyzeUrl(raw).catch(err => {
|
|
1099
|
+
blank();
|
|
1100
|
+
fail(`Unexpected error: ${err.message}`);
|
|
1101
|
+
blank();
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
});
|
|
1104
|
+
} else if (looksLikePath(raw)) {
|
|
988
1105
|
// Directory: find best content file inside it
|
|
989
1106
|
let target;
|
|
990
1107
|
try {
|
|
@@ -5,7 +5,7 @@ import { getDb } from '../db.js';
|
|
|
5
5
|
import { askClaude } from '../claude.js';
|
|
6
6
|
import { hasActiveSubscriptionLive, hasPurchased } from '../services/stripe.js';
|
|
7
7
|
// ── Usage tracking utilities ──────────────────────────────────────
|
|
8
|
-
const FREE_TIER_LIMIT =
|
|
8
|
+
const FREE_TIER_LIMIT = 50;
|
|
9
9
|
const PRO_TIER_LIMIT = 100;
|
|
10
10
|
const HEADLINE_GRADER_ENDPOINT = 'headline-grader';
|
|
11
11
|
const UPGRADE_URL = 'https://contentgrade.ai/#pricing';
|
package/package.json
CHANGED