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.
@@ -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}contentgrade.dev${R}`);
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}contentgrade.dev${R}`);
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
- if (!text || text.trim().length < 3) {
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 ($9/mo): 100 analyses/day (vs 50 free) + competitor comparison + URL audits${R}`);
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}contentgrade.dev${R}${D} → Pricing → Team${R}`);
714
- blank();
715
-
716
- process.stdout.write(` ${CY}License key:${R} `);
717
-
718
- const key = await new Promise(res => {
719
- let input = '';
720
- const isRaw = process.stdin.isTTY;
721
- if (isRaw) process.stdin.setRawMode(true);
722
- process.stdin.resume();
723
- process.stdin.setEncoding('utf8');
724
- process.stdin.on('data', function onData(chunk) {
725
- if (chunk === '\r' || chunk === '\n') {
726
- if (isRaw) process.stdin.setRawMode(false);
727
- process.stdin.pause();
728
- process.stdin.removeListener('data', onData);
729
- process.stdout.write('\n');
730
- res(input.trim());
731
- } else if (chunk === '\u0003') {
732
- if (isRaw) process.stdin.setRawMode(false);
733
- process.stdin.pause();
734
- process.stdout.write('\n');
735
- process.exit(0);
736
- } else if (chunk === '\u007f') {
737
- if (input.length > 0) { input = input.slice(0, -1); process.stdout.write('\b \b'); }
738
- } else {
739
- input += chunk;
740
- process.stdout.write('*');
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(`License activated! Pro features unlocked.`);
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}contentgrade.dev${R}`);
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}https://github.com/StanislavBG/Content-Grade${R}`);
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 start${R} then click Upgrade`);
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 with generic writing.${R}`);
1131
- console.log(` ${D}Generic content scores lowthat's the point. Watch what ContentGrade finds.${R}`);
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}Now try it on your own content:${R}`);
1329
+ console.log(` ${B}${CY}Your turn — try it on real content:${R}`);
1142
1330
  blank();
1143
- console.log(` ${CY}content-grade analyze ./your-post.md${R} ${D}# any .md or .txt file${R}`);
1144
- console.log(` ${CY}content-grade https://your-blog.com/post${R} ${D}# analyze any URL${R}`);
1145
- console.log(` ${CY}content-grade headline "Your headline here"${R} ${D}# grade a headline${R}`);
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/StanislavBG/Content-Grade)' }, timeout: 15000 }, (res) => {
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 — run demo immediately for instant value
1442
- recordEvent({ event: 'command', command: 'demo' });
1443
- cmdDemo().catch(err => {
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
- blank();
1496
- fail(`Unknown command: ${B}${raw}${R}`);
1497
- blank();
1498
- console.log(` Available commands:`);
1499
- console.log(` ${CY}analyze <file>${R} ${CY}headline "<text>"${R} ${CY}demo${R} ${CY}start${R} ${CY}init${R} ${CY}help${R}`);
1500
- blank();
1501
- console.log(` ${D}Pass a file/directory directly: ${CY}content-grade ./my-post.md${R}`);
1502
- console.log(` ${D}Run ${CY}content-grade help${R}${D} for full usage${R}`);
1503
- blank();
1504
- process.exit(1);
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: set CONTENT_GRADE_TELEMETRY_URL to enable remote reporting
21
- const REMOTE_URL = process.env.CONTENT_GRADE_TELEMETRY_URL || null;
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