content-grade 1.0.4 → 1.0.6
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 +87 -20
- package/bin/content-grade.js +326 -63
- package/bin/telemetry.js +4 -2
- package/dist/assets/index-Bc3ZrBgH.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 +7 -7
- package/dist/assets/index-BUN69TiT.js +0 -78
package/bin/content-grade.js
CHANGED
|
@@ -320,6 +320,8 @@ async function cmdAnalyze(filePath) {
|
|
|
320
320
|
console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
|
|
321
321
|
console.log(` ${D}· Unlock 100 checks/day: ${CY}content-grade activate${R}`);
|
|
322
322
|
blank();
|
|
323
|
+
console.log(` ${MG}Unlock unlimited analyses →${R} ${CY}content-grade.github.io/Content-Grade/#pricing${R}`);
|
|
324
|
+
blank();
|
|
323
325
|
process.exit(1);
|
|
324
326
|
}
|
|
325
327
|
|
|
@@ -464,7 +466,7 @@ async function cmdAnalyze(filePath) {
|
|
|
464
466
|
// Upsell — show Pro path after user has seen value
|
|
465
467
|
blank();
|
|
466
468
|
if (isProUser()) {
|
|
467
|
-
console.log(` ${D}Pro active · ${CY}
|
|
469
|
+
console.log(` ${D}Pro active · ${CY}content-grade.github.io/Content-Grade${R}`);
|
|
468
470
|
} else {
|
|
469
471
|
const usage = getUsage();
|
|
470
472
|
const remaining = Math.max(0, FREE_DAILY_LIMIT - usage.count);
|
|
@@ -472,7 +474,10 @@ async function cmdAnalyze(filePath) {
|
|
|
472
474
|
console.log(` ${MG}${B}Unlock batch mode:${R} ${CY}content-grade activate${R}`);
|
|
473
475
|
console.log(` ${D} · Analyze entire directories in one command${R}`);
|
|
474
476
|
console.log(` ${D} · 100 checks/day (${remaining} remaining today on free tier)${R}`);
|
|
475
|
-
console.log(` ${D} · Get a license at ${CY}
|
|
477
|
+
console.log(` ${D} · Get a license at ${CY}content-grade.github.io/Content-Grade${R}`);
|
|
478
|
+
blank();
|
|
479
|
+
console.log(` ${MG}Unlock unlimited analyses →${R} ${CY}content-grade.github.io/Content-Grade/#pricing${R}`);
|
|
480
|
+
console.log(` ${D}⭐ Unlock deeper analysis →${R} ${CY}npm i content-grade && content-grade activate${R} ${D}| content-grade.github.io/Content-Grade/${R}`);
|
|
476
481
|
}
|
|
477
482
|
blank();
|
|
478
483
|
}
|
|
@@ -501,7 +506,10 @@ Return ONLY valid JSON:
|
|
|
501
506
|
Use full range: generic = <35, good = 65-79, great = 80+.`;
|
|
502
507
|
|
|
503
508
|
async function cmdHeadline(text) {
|
|
504
|
-
|
|
509
|
+
// Strip non-printable control characters (keep tab/newline/carriage-return)
|
|
510
|
+
if (text) text = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').trim();
|
|
511
|
+
|
|
512
|
+
if (!text || text.length < 3) {
|
|
505
513
|
blank();
|
|
506
514
|
fail(`No headline provided.`);
|
|
507
515
|
blank();
|
|
@@ -516,6 +524,17 @@ async function cmdHeadline(text) {
|
|
|
516
524
|
process.exit(2);
|
|
517
525
|
}
|
|
518
526
|
|
|
527
|
+
// Guard: reject oversized input
|
|
528
|
+
const MAX_HEADLINE_BYTES = 2000;
|
|
529
|
+
if (text.length > MAX_HEADLINE_BYTES) {
|
|
530
|
+
blank();
|
|
531
|
+
fail(`Headline too long (${text.length} chars). Maximum is ${MAX_HEADLINE_BYTES} characters.`);
|
|
532
|
+
blank();
|
|
533
|
+
console.log(` ${YL}Tip:${R} Trim your headline to the core message and try again.`);
|
|
534
|
+
blank();
|
|
535
|
+
process.exit(2);
|
|
536
|
+
}
|
|
537
|
+
|
|
519
538
|
// Free tier daily limit
|
|
520
539
|
const limitCheck = checkDailyLimit();
|
|
521
540
|
if (!limitCheck.ok) {
|
|
@@ -526,6 +545,8 @@ async function cmdHeadline(text) {
|
|
|
526
545
|
console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
|
|
527
546
|
console.log(` ${D}· Unlock 100 checks/day: ${CY}content-grade activate${R}`);
|
|
528
547
|
blank();
|
|
548
|
+
console.log(` ${MG}Unlock unlimited analyses →${R} ${CY}content-grade.github.io/Content-Grade/#pricing${R}`);
|
|
549
|
+
blank();
|
|
529
550
|
process.exit(1);
|
|
530
551
|
}
|
|
531
552
|
|
|
@@ -603,6 +624,11 @@ async function cmdHeadline(text) {
|
|
|
603
624
|
hr();
|
|
604
625
|
console.log(` ${D}Compare two headlines: ${CY}content-grade start${R} → HeadlineGrader compare${R}`);
|
|
605
626
|
blank();
|
|
627
|
+
if (!isProUser()) {
|
|
628
|
+
console.log(` ${MG}Unlock unlimited analyses →${R} ${CY}content-grade.github.io/Content-Grade/#pricing${R}`);
|
|
629
|
+
console.log(` ${D}⭐ Unlock deeper analysis →${R} ${CY}npm i content-grade && content-grade activate${R} ${D}| content-grade.github.io/Content-Grade/${R}`);
|
|
630
|
+
blank();
|
|
631
|
+
}
|
|
606
632
|
}
|
|
607
633
|
|
|
608
634
|
// ── Init command ──────────────────────────────────────────────────────────────
|
|
@@ -681,7 +707,7 @@ async function cmdInit() {
|
|
|
681
707
|
console.log(` ${CY}content-grade start${R}`);
|
|
682
708
|
blank();
|
|
683
709
|
}
|
|
684
|
-
console.log(` ${D}Pro tier
|
|
710
|
+
console.log(` ${D}Pro tier: 100 analyses/day + batch mode — ${CY}content-grade.github.io/Content-Grade${R}`);
|
|
685
711
|
blank();
|
|
686
712
|
}
|
|
687
713
|
|
|
@@ -710,37 +736,45 @@ async function cmdActivate() {
|
|
|
710
736
|
console.log(` ${D} · ${CY}content-grade batch <dir>${R}${D} — analyze entire directories${R}`);
|
|
711
737
|
console.log(` ${D} · 100 checks/day (vs 50 free)${R}`);
|
|
712
738
|
blank();
|
|
713
|
-
console.log(` ${D}Get a license: ${CY}
|
|
714
|
-
blank();
|
|
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
|
-
|
|
739
|
+
console.log(` ${D}Get a license: ${CY}content-grade.github.io/Content-Grade${R}${D} → Pricing${R}`);
|
|
740
|
+
blank();
|
|
741
|
+
|
|
742
|
+
let key;
|
|
743
|
+
if (args[1] && args[1].length >= 8) {
|
|
744
|
+
key = args[1];
|
|
745
|
+
blank();
|
|
746
|
+
console.log(` ${D}Using provided key: ${args[1].slice(0, 8)}...${R}`);
|
|
747
|
+
blank();
|
|
748
|
+
} else {
|
|
749
|
+
process.stdout.write(` ${CY}License key:${R} `);
|
|
750
|
+
|
|
751
|
+
key = await new Promise(res => {
|
|
752
|
+
let input = '';
|
|
753
|
+
const isRaw = process.stdin.isTTY;
|
|
754
|
+
if (isRaw) process.stdin.setRawMode(true);
|
|
755
|
+
process.stdin.resume();
|
|
756
|
+
process.stdin.setEncoding('utf8');
|
|
757
|
+
process.stdin.on('data', function onData(chunk) {
|
|
758
|
+
if (chunk === '\r' || chunk === '\n') {
|
|
759
|
+
if (isRaw) process.stdin.setRawMode(false);
|
|
760
|
+
process.stdin.pause();
|
|
761
|
+
process.stdin.removeListener('data', onData);
|
|
762
|
+
process.stdout.write('\n');
|
|
763
|
+
res(input.trim());
|
|
764
|
+
} else if (chunk === '\u0003') {
|
|
765
|
+
if (isRaw) process.stdin.setRawMode(false);
|
|
766
|
+
process.stdin.pause();
|
|
767
|
+
process.stdout.write('\n');
|
|
768
|
+
process.exit(0);
|
|
769
|
+
} else if (chunk === '\u007f') {
|
|
770
|
+
if (input.length > 0) { input = input.slice(0, -1); process.stdout.write('\b \b'); }
|
|
771
|
+
} else {
|
|
772
|
+
input += chunk;
|
|
773
|
+
process.stdout.write('*');
|
|
774
|
+
}
|
|
775
|
+
});
|
|
742
776
|
});
|
|
743
|
-
}
|
|
777
|
+
}
|
|
744
778
|
|
|
745
779
|
if (!key || key.length < 8) {
|
|
746
780
|
blank();
|
|
@@ -749,13 +783,45 @@ async function cmdActivate() {
|
|
|
749
783
|
process.exit(2);
|
|
750
784
|
}
|
|
751
785
|
|
|
786
|
+
// Validate key against server before storing
|
|
787
|
+
const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://contentgrade.ai';
|
|
788
|
+
blank();
|
|
789
|
+
process.stdout.write(` ${D}Validating key...${R}`);
|
|
790
|
+
|
|
791
|
+
let validated = false;
|
|
792
|
+
try {
|
|
793
|
+
const response = await fetch(`${serverUrl}/api/license/validate`, {
|
|
794
|
+
method: 'POST',
|
|
795
|
+
headers: { 'Content-Type': 'application/json' },
|
|
796
|
+
body: JSON.stringify({ key }),
|
|
797
|
+
});
|
|
798
|
+
const data = await response.json();
|
|
799
|
+
if (data.valid) {
|
|
800
|
+
validated = true;
|
|
801
|
+
} else {
|
|
802
|
+
process.stdout.write('\n');
|
|
803
|
+
blank();
|
|
804
|
+
fail(data.message || 'Invalid or expired license key. Visit contentgrade.ai to check your subscription.');
|
|
805
|
+
blank();
|
|
806
|
+
process.exit(2);
|
|
807
|
+
}
|
|
808
|
+
} catch {
|
|
809
|
+
// Server unreachable — allow offline activation with a warning
|
|
810
|
+
process.stdout.write('\n');
|
|
811
|
+
blank();
|
|
812
|
+
console.log(` ${D}⚠ Could not reach server — storing key locally without verification.${R}`);
|
|
813
|
+
validated = true;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
process.stdout.write(' done\n');
|
|
817
|
+
|
|
752
818
|
const config = loadConfig();
|
|
753
819
|
config.licenseKey = key;
|
|
754
820
|
config.activatedAt = new Date().toISOString();
|
|
755
821
|
saveConfig(config);
|
|
756
822
|
|
|
757
823
|
blank();
|
|
758
|
-
ok(`
|
|
824
|
+
ok(`Pro access activated! Your daily limit is now 100 analyses.`);
|
|
759
825
|
blank();
|
|
760
826
|
console.log(` ${B}Try it:${R}`);
|
|
761
827
|
console.log(` ${CY} content-grade batch ./posts/${R} ${D}# analyze all files${R}`);
|
|
@@ -777,7 +843,7 @@ async function cmdBatch(dirPath) {
|
|
|
777
843
|
console.log(` ${B}Unlock batch mode:${R}`);
|
|
778
844
|
console.log(` ${CY}content-grade activate${R} ${D}(enter your license key)${R}`);
|
|
779
845
|
blank();
|
|
780
|
-
console.log(` ${D}Get a license: ${CY}
|
|
846
|
+
console.log(` ${D}Get a license: ${CY}content-grade.github.io/Content-Grade${R}`);
|
|
781
847
|
blank();
|
|
782
848
|
process.exit(1);
|
|
783
849
|
}
|
|
@@ -845,6 +911,20 @@ async function cmdBatch(dirPath) {
|
|
|
845
911
|
const rel = f.startsWith(process.cwd()) ? f.slice(process.cwd().length + 1) : f;
|
|
846
912
|
process.stdout.write(` ${D}[${i + 1}/${files.length}]${R} ${rel}...`);
|
|
847
913
|
|
|
914
|
+
// Guard: check daily limit before each analysis
|
|
915
|
+
const batchLimitCheck = checkDailyLimit();
|
|
916
|
+
if (!batchLimitCheck.ok) {
|
|
917
|
+
process.stdout.write(` ${YL}limit reached${R}\n`);
|
|
918
|
+
blank();
|
|
919
|
+
warn(`Daily limit reached (${batchLimitCheck.count}/${batchLimitCheck.limit}). Remaining files skipped.`);
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Guard: skip files over 500 KB to avoid OOM on large files
|
|
924
|
+
let fileStat;
|
|
925
|
+
try { fileStat = statSync(f); } catch { process.stdout.write(` ${RD}unreadable${R}\n`); continue; }
|
|
926
|
+
if (fileStat.size > 500 * 1024) { process.stdout.write(` ${YL}too large${R}\n`); continue; }
|
|
927
|
+
|
|
848
928
|
let content;
|
|
849
929
|
try { content = readFileSync(f, 'utf8'); } catch { process.stdout.write(` ${RD}unreadable${R}\n`); continue; }
|
|
850
930
|
if (content.trim().length < 20 || content.includes('\x00')) { process.stdout.write(` ${YL}skipped${R}\n`); continue; }
|
|
@@ -1008,8 +1088,16 @@ function cmdStart() {
|
|
|
1008
1088
|
|
|
1009
1089
|
function cmdHelp() {
|
|
1010
1090
|
banner();
|
|
1011
|
-
console.log(` ${D}v${_version} · ${CY}
|
|
1091
|
+
console.log(` ${D}v${_version} · ${CY}content-grade.github.io/Content-Grade${R}`);
|
|
1092
|
+
blank();
|
|
1093
|
+
console.log(` ${B}QUICK START${R}`);
|
|
1012
1094
|
blank();
|
|
1095
|
+
console.log(` ${CY}npx content-grade headline "Your title here"${R} ${D}# grade a headline (fastest, ~5s)${R}`);
|
|
1096
|
+
console.log(` ${CY}npx content-grade analyze ./post.md${R} ${D}# full AI content analysis (~20s)${R}`);
|
|
1097
|
+
console.log(` ${CY}npx content-grade demo${R} ${D}# live demo on sample content${R}`);
|
|
1098
|
+
console.log(` ${CY}npx content-grade start${R} ${D}# launch web dashboard (6 tools)${R}`);
|
|
1099
|
+
blank();
|
|
1100
|
+
|
|
1013
1101
|
console.log(` ${B}USAGE${R}`);
|
|
1014
1102
|
blank();
|
|
1015
1103
|
console.log(` ${CY}content-grade <command> [args]${R}`);
|
|
@@ -1088,10 +1176,110 @@ function cmdHelp() {
|
|
|
1088
1176
|
console.log(` · Email subject line optimizer`);
|
|
1089
1177
|
console.log(` · Audience archetype decoder`);
|
|
1090
1178
|
blank();
|
|
1091
|
-
console.log(` ${CY}content-grade
|
|
1179
|
+
console.log(` ${CY}content-grade.github.io/Content-Grade${R} → then: ${CY}content-grade activate${R}`);
|
|
1092
1180
|
blank();
|
|
1093
1181
|
}
|
|
1094
1182
|
|
|
1183
|
+
// ── First-run detection ───────────────────────────────────────────────────────
|
|
1184
|
+
|
|
1185
|
+
function isFirstRun() {
|
|
1186
|
+
return !existsSync(CONFIG_FILE);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// ── Quick Demo (instant, no Claude required) ──────────────────────────────────
|
|
1190
|
+
|
|
1191
|
+
function cmdQuickDemo() {
|
|
1192
|
+
blank();
|
|
1193
|
+
if (isFirstRun()) {
|
|
1194
|
+
console.log(`${B}${CY} Welcome to ContentGrade!${R}`);
|
|
1195
|
+
console.log(` ${D}Grade any blog post, landing page, or ad copy in seconds.${R}`);
|
|
1196
|
+
blank();
|
|
1197
|
+
console.log(` ${D}Here's a sample analysis — then try it on your own content.${R}`);
|
|
1198
|
+
} else {
|
|
1199
|
+
console.log(`${B}${CY} ContentGrade${R} ${D}— Sample Analysis (instant, no Claude needed)${R}`);
|
|
1200
|
+
console.log(` ${D}Run ${CY}npx content-grade analyze ./your-post.md${R}${D} for a real AI-powered result.${R}`);
|
|
1201
|
+
}
|
|
1202
|
+
blank();
|
|
1203
|
+
|
|
1204
|
+
// Pre-rendered analysis of DEMO_CONTENT — identical format to real cmdAnalyze output
|
|
1205
|
+
console.log(` ${D}Analyzing: "How to Write Better Blog Posts" (generic blog post)${R}`);
|
|
1206
|
+
blank();
|
|
1207
|
+
hr();
|
|
1208
|
+
|
|
1209
|
+
// Overall score
|
|
1210
|
+
console.log(` ${B}OVERALL SCORE${R} ${YL}${B}42/100${R} ${RD}${B}D${R} ${D}blog post${R}`);
|
|
1211
|
+
console.log(` ${scoreBar(42, 40)}`);
|
|
1212
|
+
blank();
|
|
1213
|
+
|
|
1214
|
+
// Headline
|
|
1215
|
+
console.log(` ${B}HEADLINE SCORE${R} ${scoreBar(35, 20)} 35/100`);
|
|
1216
|
+
console.log(` ${D}"How to Write Better Blog Posts"${R}`);
|
|
1217
|
+
console.log(` ${D}↳ Generic title with no specificity, number, or benefit — reads like every other guide.${R}`);
|
|
1218
|
+
blank();
|
|
1219
|
+
|
|
1220
|
+
hr();
|
|
1221
|
+
console.log(` ${B}DIMENSION BREAKDOWN${R}`);
|
|
1222
|
+
blank();
|
|
1223
|
+
const _demodims = [
|
|
1224
|
+
{ label: 'Clarity', score: 55, note: 'Sentences are clear but vague — "good" and "better" appear 12× with no definition.' },
|
|
1225
|
+
{ label: 'Engagement', score: 38, note: 'No hook, no tension. Opens with a passive statement any writing guide could claim.' },
|
|
1226
|
+
{ label: 'Structure', score: 45, note: 'Numbered list helps scannability but every section carries equal weight — no pyramid.' },
|
|
1227
|
+
{ label: 'Value Delivery', score: 30, note: '"Write good content" is not advice. Every tip is a truism with no specific technique.' },
|
|
1228
|
+
];
|
|
1229
|
+
for (const d of _demodims) {
|
|
1230
|
+
console.log(` ${WH}${d.label.padEnd(16)}${R} ${scoreBar(d.score, 20)} ${d.score}/100`);
|
|
1231
|
+
console.log(` ${D} ↳ ${d.note}${R}`);
|
|
1232
|
+
blank();
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
hr();
|
|
1236
|
+
console.log(` ${B}VERDICT${R}`);
|
|
1237
|
+
console.log(` ${YL}Technically correct but forgettable — every sentence could be cut in half and the advice doubled in specificity.${R}`);
|
|
1238
|
+
blank();
|
|
1239
|
+
|
|
1240
|
+
hr();
|
|
1241
|
+
console.log(` ${B}STRENGTHS${R}`);
|
|
1242
|
+
console.log(` ${GN}+${R} Logical four-step structure is easy to follow`);
|
|
1243
|
+
console.log(` ${GN}+${R} Short paragraphs with good white space`);
|
|
1244
|
+
blank();
|
|
1245
|
+
|
|
1246
|
+
hr();
|
|
1247
|
+
console.log(` ${B}TOP IMPROVEMENTS${R}`);
|
|
1248
|
+
blank();
|
|
1249
|
+
console.log(` ${RD}●${R} ${B}Headline has no specificity${R}`);
|
|
1250
|
+
console.log(` ${CY}Fix:${R} Add a number and outcome: "7 Blog Post Mistakes That Kill Readership (And the Fixes That Work)"`);
|
|
1251
|
+
blank();
|
|
1252
|
+
console.log(` ${RD}●${R} ${B}Tips are high-level truisms${R}`);
|
|
1253
|
+
console.log(` ${CY}Fix:${R} Replace "write good content" with a technique: "Open with the reader's frustration, not your advice."`);
|
|
1254
|
+
blank();
|
|
1255
|
+
console.log(` ${YL}●${R} ${B}No proof or examples${R}`);
|
|
1256
|
+
console.log(` ${CY}Fix:${R} Add a before/after example for one tip to make the advice concrete and credible`);
|
|
1257
|
+
blank();
|
|
1258
|
+
|
|
1259
|
+
hr();
|
|
1260
|
+
console.log(` ${B}HEADLINE REWRITES${R}`);
|
|
1261
|
+
blank();
|
|
1262
|
+
console.log(` ${D}1.${R} 7 Blog Post Mistakes That Kill Your Readership (And the Fixes That Work)`);
|
|
1263
|
+
console.log(` ${D}2.${R} How to Write Blog Posts That People Actually Finish Reading`);
|
|
1264
|
+
blank();
|
|
1265
|
+
|
|
1266
|
+
hr();
|
|
1267
|
+
console.log(` ${D}↑ This is a ${B}sample${R}${D} — your real content gets a live AI analysis in ~20 seconds.${R}`);
|
|
1268
|
+
blank();
|
|
1269
|
+
console.log(` ${B}${CY}Try it now:${R}`);
|
|
1270
|
+
blank();
|
|
1271
|
+
console.log(` ${CY}npx content-grade headline "Your headline here"${R} ${D}# fastest — grade any headline${R}`);
|
|
1272
|
+
console.log(` ${CY}npx content-grade analyze ./your-post.md${R} ${D}# full AI analysis of a file${R}`);
|
|
1273
|
+
console.log(` ${CY}npx content-grade demo${R} ${D}# live demo — takes ~20s with Claude${R}`);
|
|
1274
|
+
blank();
|
|
1275
|
+
|
|
1276
|
+
if (isFirstRun()) {
|
|
1277
|
+
console.log(` ${D}Requires Claude CLI (free): ${CY}claude.ai/code${R}${D} → install → ${CY}claude login${R}`);
|
|
1278
|
+
console.log(` ${D}Setup check: ${CY}npx content-grade init${R}`);
|
|
1279
|
+
blank();
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1095
1283
|
// ── Demo command ──────────────────────────────────────────────────────────────
|
|
1096
1284
|
|
|
1097
1285
|
const DEMO_CONTENT = `# How to Write Better Blog Posts
|
|
@@ -1127,8 +1315,8 @@ async function cmdDemo() {
|
|
|
1127
1315
|
banner();
|
|
1128
1316
|
console.log(` ${B}Demo Mode${R} ${D}— see ContentGrade in action${R}`);
|
|
1129
1317
|
blank();
|
|
1130
|
-
console.log(` ${D}Analyzing a sample blog post
|
|
1131
|
-
console.log(` ${D}
|
|
1318
|
+
console.log(` ${D}Analyzing a sample blog post. Watch ContentGrade identify exactly what's weak${R}`);
|
|
1319
|
+
console.log(` ${D}and how to fix it — this is what you'll see on your own content.${R}`);
|
|
1132
1320
|
blank();
|
|
1133
1321
|
|
|
1134
1322
|
writeFileSync(tmpFile, DEMO_CONTENT, 'utf8');
|
|
@@ -1138,11 +1326,11 @@ async function cmdDemo() {
|
|
|
1138
1326
|
// Post-demo CTA — shown AFTER user has seen the analysis output
|
|
1139
1327
|
blank();
|
|
1140
1328
|
hr();
|
|
1141
|
-
console.log(` ${B}${CY}
|
|
1329
|
+
console.log(` ${B}${CY}Your turn — try it on real content:${R}`);
|
|
1142
1330
|
blank();
|
|
1143
|
-
console.log(` ${CY}content-grade
|
|
1144
|
-
console.log(` ${CY}content-grade https://
|
|
1145
|
-
console.log(` ${CY}content-grade headline "Your
|
|
1331
|
+
console.log(` ${CY}npx content-grade ./your-post.md${R} ${D}# score any .md or .txt file${R}`);
|
|
1332
|
+
console.log(` ${CY}npx content-grade https://example.com/blog/post${R} ${D}# fetch and score any URL${R}`);
|
|
1333
|
+
console.log(` ${CY}npx content-grade headline "Your title here"${R} ${D}# grade a single headline${R}`);
|
|
1146
1334
|
blank();
|
|
1147
1335
|
|
|
1148
1336
|
// Show telemetry notice after user has seen value (first run only)
|
|
@@ -1150,6 +1338,55 @@ async function cmdDemo() {
|
|
|
1150
1338
|
console.log(` ${D}Anonymous usage data is collected by default. Opt out: ${CY}content-grade telemetry off${R}`);
|
|
1151
1339
|
blank();
|
|
1152
1340
|
}
|
|
1341
|
+
|
|
1342
|
+
// Interactive follow-up — strike while the iron is hot
|
|
1343
|
+
if (process.stdin.isTTY) {
|
|
1344
|
+
hr();
|
|
1345
|
+
console.log(` ${B}${CY}Score your own content right now:${R}`);
|
|
1346
|
+
blank();
|
|
1347
|
+
console.log(` ${D}Paste a URL (https://...), a headline, or a file path — press Enter to analyze.${R}`);
|
|
1348
|
+
console.log(` ${D}Or press Enter to skip.${R}`);
|
|
1349
|
+
blank();
|
|
1350
|
+
process.stdout.write(` ${CY}>${R} `);
|
|
1351
|
+
|
|
1352
|
+
const userInput = await new Promise(res => {
|
|
1353
|
+
let line = '';
|
|
1354
|
+
process.stdin.resume();
|
|
1355
|
+
process.stdin.setEncoding('utf8');
|
|
1356
|
+
const onData = (chunk) => {
|
|
1357
|
+
const str = chunk.toString();
|
|
1358
|
+
if (str.includes('\n') || str.includes('\r')) {
|
|
1359
|
+
process.stdin.pause();
|
|
1360
|
+
process.stdin.removeListener('data', onData);
|
|
1361
|
+
process.stdout.write('\n');
|
|
1362
|
+
res(line.replace(/[\r\n]/g, '').trim());
|
|
1363
|
+
} else if (str === '\u0003') {
|
|
1364
|
+
process.stdin.pause();
|
|
1365
|
+
process.stdin.removeListener('data', onData);
|
|
1366
|
+
process.stdout.write('\n');
|
|
1367
|
+
res('');
|
|
1368
|
+
} else {
|
|
1369
|
+
line += str;
|
|
1370
|
+
process.stdout.write(str);
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
process.stdin.on('data', onData);
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
if (userInput) {
|
|
1377
|
+
blank();
|
|
1378
|
+
if (/^https?:\/\//i.test(userInput)) {
|
|
1379
|
+
await cmdAnalyzeUrl(userInput);
|
|
1380
|
+
} else if (looksLikePath(userInput)) {
|
|
1381
|
+
await cmdAnalyze(userInput);
|
|
1382
|
+
} else if (userInput.length >= 5) {
|
|
1383
|
+
await cmdHeadline(userInput);
|
|
1384
|
+
} else {
|
|
1385
|
+
warn(`Couldn't parse that. Try a URL like https://... or a headline like "Your title here".`);
|
|
1386
|
+
blank();
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1153
1390
|
} finally {
|
|
1154
1391
|
try { unlinkSync(tmpFile); } catch {}
|
|
1155
1392
|
}
|
|
@@ -1160,7 +1397,7 @@ async function cmdDemo() {
|
|
|
1160
1397
|
function fetchUrl(url) {
|
|
1161
1398
|
return new Promise((resolve, reject) => {
|
|
1162
1399
|
const get = url.startsWith('https') ? httpsGet : httpGet;
|
|
1163
|
-
const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/
|
|
1400
|
+
const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/content-grade/Content-Grade)' }, timeout: 15000 }, (res) => {
|
|
1164
1401
|
// Follow one redirect
|
|
1165
1402
|
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
|
|
1166
1403
|
fetchUrl(res.headers.location).then(resolve).catch(reject);
|
|
@@ -1201,6 +1438,22 @@ function htmlToText(html) {
|
|
|
1201
1438
|
}
|
|
1202
1439
|
|
|
1203
1440
|
async function cmdAnalyzeUrl(url) {
|
|
1441
|
+
// Validate and sanitize URL input
|
|
1442
|
+
const sanitizedUrl = (url || '').replace(/[\x00-\x1F\x7F]/g, '').trim();
|
|
1443
|
+
if (!sanitizedUrl || !/^https?:\/\/.{3,}/i.test(sanitizedUrl)) {
|
|
1444
|
+
blank();
|
|
1445
|
+
fail(`Invalid URL: must start with http:// or https://`);
|
|
1446
|
+
blank();
|
|
1447
|
+
process.exit(2);
|
|
1448
|
+
}
|
|
1449
|
+
if (sanitizedUrl.length > 2048) {
|
|
1450
|
+
blank();
|
|
1451
|
+
fail(`URL too long (${sanitizedUrl.length} chars). Maximum is 2048.`);
|
|
1452
|
+
blank();
|
|
1453
|
+
process.exit(2);
|
|
1454
|
+
}
|
|
1455
|
+
url = sanitizedUrl;
|
|
1456
|
+
|
|
1204
1457
|
banner();
|
|
1205
1458
|
console.log(` ${B}Analyzing URL:${R} ${CY}${url}${R}`);
|
|
1206
1459
|
blank();
|
|
@@ -1438,14 +1691,9 @@ switch (cmd) {
|
|
|
1438
1691
|
}
|
|
1439
1692
|
|
|
1440
1693
|
case undefined:
|
|
1441
|
-
// No command —
|
|
1442
|
-
recordEvent({ event: 'command', command: '
|
|
1443
|
-
|
|
1444
|
-
blank();
|
|
1445
|
-
fail(`Demo error: ${err.message}`);
|
|
1446
|
-
blank();
|
|
1447
|
-
process.exit(1);
|
|
1448
|
-
});
|
|
1694
|
+
// No command — show instant static demo (no Claude required, zero wait)
|
|
1695
|
+
recordEvent({ event: 'command', command: 'quick_demo' });
|
|
1696
|
+
cmdQuickDemo();
|
|
1449
1697
|
break;
|
|
1450
1698
|
|
|
1451
1699
|
default:
|
|
@@ -1492,15 +1740,30 @@ switch (cmd) {
|
|
|
1492
1740
|
process.exit(1);
|
|
1493
1741
|
});
|
|
1494
1742
|
} else {
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1743
|
+
// Smart fallback: treat unrecognised input as a headline to grade.
|
|
1744
|
+
// Covers: npx content-grade "Why most startups fail"
|
|
1745
|
+
// npx content-grade Why most startups fail
|
|
1746
|
+
const allText = args.join(' ').trim();
|
|
1747
|
+
if (allText && allText.length >= 5 && !allText.startsWith('-')) {
|
|
1748
|
+
recordEvent({ event: 'command', command: 'headline_smart' });
|
|
1749
|
+
cmdHeadline(allText).catch(err => {
|
|
1750
|
+
blank();
|
|
1751
|
+
fail(`Unexpected error: ${err.message}`);
|
|
1752
|
+
blank();
|
|
1753
|
+
process.exit(1);
|
|
1754
|
+
});
|
|
1755
|
+
} else {
|
|
1756
|
+
blank();
|
|
1757
|
+
fail(`Unknown command: ${B}${raw}${R}`);
|
|
1758
|
+
blank();
|
|
1759
|
+
console.log(` Available commands:`);
|
|
1760
|
+
console.log(` ${CY}analyze <file>${R} ${CY}headline "<text>"${R} ${CY}demo${R} ${CY}start${R} ${CY}init${R} ${CY}help${R}`);
|
|
1761
|
+
blank();
|
|
1762
|
+
console.log(` ${D}Pass a file/directory directly: ${CY}content-grade ./my-post.md${R}`);
|
|
1763
|
+
console.log(` ${D}Grade a headline: ${CY}content-grade "Your headline text"${R}`);
|
|
1764
|
+
console.log(` ${D}Run ${CY}content-grade help${R}${D} for full usage${R}`);
|
|
1765
|
+
blank();
|
|
1766
|
+
process.exit(1);
|
|
1767
|
+
}
|
|
1505
1768
|
}
|
|
1506
1769
|
}
|
package/bin/telemetry.js
CHANGED
|
@@ -17,8 +17,10 @@ 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
|
+
// Set CONTENT_GRADE_TELEMETRY_URL to enable remote aggregation (e.g. self-hosted endpoint).
|
|
22
|
+
// Default: local-only (events stored at ~/.content-grade/events.jsonl, no remote send).
|
|
23
|
+
const REMOTE_URL = process.env.CONTENT_GRADE_TELEMETRY_URL || '';
|
|
22
24
|
|
|
23
25
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
24
26
|
|