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 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. Grade a single headline
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
- Free analysis complete. Pro unlocks:
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: **3 analyses/day per tool**. Pro ($9/mo): **100 analyses/day** + all tools.
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": 3,
541
- "message": "Free daily limit reached (3/day). Get 100 analyses/day with Pro"
540
+ "limit": 50,
541
+ "message": "Free daily limit reached (50/day). Get 100 analyses/day with Pro"
542
542
  }
543
543
  ```
544
544
 
@@ -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(1);
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(1);
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(1);
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(1);
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(1);
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(1);
245
+ process.exit(2);
244
246
  }
245
247
 
246
- banner();
247
- console.log(` ${B}Analyzing:${R} ${CY}${basename(absPath)}${R}`);
248
- console.log(` ${D}${content.length.toLocaleString()} characters · detecting content type...${R}`);
249
- blank();
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
- // ── Pro teaser
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}Start the web dashboard: ${CY}content-grade start${R}`);
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: 3 analyses/day. Upgrade at ${url}`);
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 3 free)`);
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(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/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 args = process.argv.slice(2).filter(a => a !== '--no-telemetry');
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
- if (_telem.isNew) {
863
- // Show notice once, on first run
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 (looksLikePath(raw)) {
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 = 3;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-grade",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "AI-powered content analysis CLI. Score any blog post, landing page, or ad copy in under 30 seconds — runs on Claude CLI, no API key needed.",
5
5
  "type": "module",
6
6
  "bin": {