content-grade 1.0.12 → 1.0.14

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/CONTRIBUTING.md CHANGED
@@ -1,198 +1,47 @@
1
- # Contributing to ContentGrade
1
+ # Contributing to Content-Grade
2
2
 
3
- Thanks for wanting to contribute. ContentGrade is a CLI + web dashboard for AI-powered content scoring. It runs entirely on local Claude CLI — no API keys, no external services (unless you add Stripe).
3
+ ## Reporting bugs
4
4
 
5
- ---
5
+ Open an issue using the **Bug report** template. Include:
6
+ - Your Node.js version (`node --version`)
7
+ - The exact command you ran
8
+ - Output of `npx content-grade init` if it's a setup issue
9
+ - Expected vs. actual behavior
6
10
 
7
- ## Prerequisites
11
+ ## Suggesting features
8
12
 
9
- - **Node.js 18+**
10
- - **pnpm** (`npm install -g pnpm`)
11
- - **Claude CLI** — installed and logged in ([claude.ai/code](https://claude.ai/code))
12
- - Verify: `claude -p "say hi"`
13
+ Open an issue using **Feature request**, or post in [GitHub Discussions → Ideas](https://github.com/StanislavBG/Content-Grade/discussions/categories/ideas). Describe the use case, not just the feature.
13
14
 
14
- ---
15
+ ## Submitting a pull request
15
16
 
16
- ## Local development setup
17
+ 1. Fork the repo, create a branch from `main`
18
+ 2. Make changes — see code style notes below
19
+ 3. Run the full check before opening a PR:
17
20
 
18
21
  ```bash
19
- git clone https://github.com/Content-Grade/Content-Grade
20
- cd Content-Grade
21
- pnpm install
22
- pnpm dev
22
+ npm test # all tests must pass
23
+ npm run typecheck # zero TypeScript errors
23
24
  ```
24
25
 
25
- This starts two processes concurrently:
26
- - **Client** (Vite) at [http://localhost:3000](http://localhost:3000) with hot reload
27
- - **Server** (tsx watch) at [http://localhost:4000](http://localhost:4000) with hot reload
26
+ 4. Open a PR with a clear description of what changed and why
28
27
 
29
- The client proxies API requests to the server automatically.
28
+ ## Code style
30
29
 
31
- ---
30
+ Scripts from `package.json` tell you what tools are in play:
32
31
 
33
- ## Project structure
32
+ | Script | What it does |
33
+ |--------|-------------|
34
+ | `npm test` | vitest — run once, all tests green |
35
+ | `npm run test:watch` | vitest watch mode during development |
36
+ | `npm run test:coverage` | coverage report (v8) |
37
+ | `npm run typecheck` | `tsc --noEmit` — types must be clean |
38
+ | `npm run build` | vite + tsc server build — must pass before release |
34
39
 
35
- ```
36
- bin/
37
- content-grade.js CLI entry point all CLI commands live here
38
- server/
39
- app.ts Fastify app factory (used by server and tests)
40
- index.ts Production server entry (binds port)
41
- claude.ts Claude CLI wrapper (server-side)
42
- db.ts SQLite setup — usage tracking, stripe subscriptions
43
- routes/
44
- demos.ts API routes for all 6 web dashboard tools
45
- stripe.ts Stripe checkout, webhooks, subscription management
46
- services/
47
- claude.ts askClaude() helper for route handlers
48
- stripe.ts Stripe helpers — subscription checks, customer management
49
- src/
50
- App.tsx React app router
51
- main.tsx React entry point
52
- views/
53
- HeadlineGraderView.tsx
54
- PageRoastView.tsx
55
- AdScorerView.tsx
56
- ThreadGraderView.tsx
57
- EmailForgeView.tsx
58
- AudienceDecoderView.tsx
59
- ContentGradeShared.tsx Shared UI components
60
- data/
61
- api.ts API client for the web dashboard
62
- tests/
63
- unit/ Unit tests (no Claude required)
64
- integration/ Integration tests (spins up Fastify)
65
- helpers/
66
- db.ts Test database helpers
67
- ```
68
-
69
- ---
70
-
71
- ## Running tests
72
-
73
- ```bash
74
- pnpm test # run all tests once
75
- pnpm test:watch # watch mode — reruns on file changes
76
- pnpm test:coverage # full coverage report
77
- ```
78
-
79
- Tests live in `tests/unit/` and `tests/integration/`. Integration tests spin up a real Fastify server but mock Claude responses — they don't require a live Claude CLI.
80
-
81
- Unit tests cover:
82
- - `tests/unit/validation.test.ts` — input validation
83
- - `tests/unit/cli-utils.test.ts` — CLI helper functions
84
- - `tests/unit/rate-limit.test.ts` — rate limit logic
85
- - `tests/unit/edge-cases.test.ts` — edge case handling
86
- - `tests/unit/quality-hardening.test.ts` — error path hardening
87
-
88
- ---
89
-
90
- ## TypeScript
91
-
92
- ```bash
93
- pnpm typecheck # run tsc --noEmit — check types without building
94
- ```
95
-
96
- The project has two TypeScript configs:
97
- - `tsconfig.json` — client (Vite, React, ESNext)
98
- - `tsconfig.server.json` — server (Node.js ESM, outputs to `dist-server/`)
99
-
100
- Both must pass before merging.
101
-
102
- ---
103
-
104
- ## Building for production
105
-
106
- ```bash
107
- pnpm build # builds client (dist/) and server (dist-server/)
108
- pnpm start # runs the production server
109
- ```
110
-
111
- After building, test the full CLI flow:
112
-
113
- ```bash
114
- node bin/content-grade.js init
115
- node bin/content-grade.js demo
116
- node bin/content-grade.js start
117
- ```
118
-
119
- ---
120
-
121
- ## Making changes
122
-
123
- ### Adding or modifying CLI commands
124
-
125
- All CLI logic lives in `bin/content-grade.js`. The router is the `switch (cmd)` block at the bottom.
126
-
127
- Pattern for a new command:
128
- 1. Add a `cmdFoo(args)` async function
129
- 2. Add a `case 'foo':` branch in the switch
130
- 3. Follow the existing error handling pattern: `blank()`, `fail()`, `process.exit(1)`
131
- 4. Use `checkClaude()` before any `askClaude()` call
132
-
133
- ### Adding a new web dashboard tool
134
-
135
- 1. Create `src/views/FooView.tsx` — follow the pattern of existing views
136
- 2. Add a route in `App.tsx`
137
- 3. Add an API endpoint in `server/routes/demos.ts`
138
- 4. Add the endpoint name to `FREE_TIER_LIMIT` tracking in `demos.ts`
139
- 5. Add to the tool list in `bin/content-grade.js` `cmdStart()` output
140
-
141
- ### Modifying Claude prompts
142
-
143
- Prompts are inline in `bin/content-grade.js` (CLI) and `server/routes/demos.ts` (web). Both require JSON-only responses — the parser handles both clean JSON and JSON embedded in markdown fences.
144
-
145
- The scoring calibration note at the bottom of each prompt is load-bearing: without it, Claude uses a compressed range and most content scores 50-70. Keep it.
146
-
147
- ---
148
-
149
- ## Claude model selection
150
-
151
- | Use case | Model | Why |
152
- |----------|-------|-----|
153
- | `analyze` command | `claude-sonnet-4-6` | Richer analysis, better calibration |
154
- | `headline` command | `claude-haiku-4-5-20251001` | Fast, good enough for structured scoring |
155
- | `init` smoke test | `claude-haiku-4-5-20251001` | Just needs a JSON ping |
156
- | Web dashboard tools | `claude-haiku-4-5-20251001` | Server-side, latency-sensitive |
157
-
158
- ---
159
-
160
- ## Rate limiting
161
-
162
- The web dashboard uses SQLite-backed IP rate limiting:
163
- - Free tier: 3 analyses/day per tool per IP (IP is SHA-256 hashed, never stored raw)
164
- - Pro tier: 100 analyses/day — verified against Stripe API with a 5-minute cache
165
- - AudienceDecoder: one-time purchase model (different Stripe Price ID)
166
-
167
- The CLI (`analyze`, `headline`) has no rate limiting — it calls Claude directly from your machine.
168
-
169
- ---
170
-
171
- ## Stripe setup (optional)
172
-
173
- The app works without Stripe — upgrade prompts show "Coming Soon" instead of a checkout button.
174
-
175
- To enable payments:
176
- 1. Copy `.env.example` to `.env`
177
- 2. Add your Stripe keys
178
- 3. Create products in Stripe dashboard — get the Price IDs
179
- 4. Add `STRIPE_PRICE_CONTENTGRADE_PRO` and `STRIPE_PRICE_AUDIENCEDECODER`
180
- 5. Set up a webhook pointing to `/api/stripe/webhook`
181
-
182
- ---
183
-
184
- ## Pull request checklist
185
-
186
- - [ ] `pnpm typecheck` passes (zero TypeScript errors)
187
- - [ ] `pnpm test` passes (all tests green)
188
- - [ ] CLI smoke test passes: `node bin/content-grade.js demo`
189
- - [ ] No hardcoded API keys or credentials
190
- - [ ] Error paths follow the existing pattern (`blank()`, `fail()`, `process.exit(1)`)
191
- - [ ] New Claude prompts include the scoring calibration note
192
- - [ ] PR description explains what changed and why
193
-
194
- ---
40
+ - TypeScript throughout — no `any` without a comment explaining why
41
+ - CLI errors use `fail()` helper, never raw `console.error` + `process.exit`
42
+ - New Claude prompts must include the scoring calibration note (see existing prompts)
43
+ - No hardcoded API keys or credentials anywhere
195
44
 
196
45
  ## Questions
197
46
 
198
- Open an issue or start a GitHub Discussion. Include your Node.js version, OS, and the output of `npx content-grade init` if it's a setup issue.
47
+ Post in [GitHub Discussions Q&A](https://github.com/StanislavBG/Content-Grade/discussions/categories/q-a).
package/README.md CHANGED
@@ -63,7 +63,7 @@ npx content-grade 'blog/**/*.md'
63
63
 
64
64
  **JSON output for CI integration:**
65
65
  ```bash
66
- npx content-grade README.md --format json
66
+ npx content-grade README.md --json
67
67
  # Returns structured JSON with score, grade, dimensions, and improvements
68
68
  # Exit code 1 if score < 50 — useful for blocking low-quality merges
69
69
  ```
@@ -549,6 +549,7 @@ tests/
549
549
 
550
550
  **Documentation:**
551
551
  - [`docs/getting-started.md`](./docs/getting-started.md) — step-by-step first-run guide
552
+ - [`docs/cli-reference.md`](./docs/cli-reference.md) — full CLI reference: all commands, flags, output schema, exit codes
552
553
  - [`docs/examples.md`](./docs/examples.md) — real-world examples for all 6 tools, CI/CD workflows, Node.js integration
553
554
  - [`docs/api.md`](./docs/api.md) — full REST API reference
554
555
 
@@ -974,6 +975,8 @@ Power users who go beyond early adoption get early access to `@beta` releases, a
974
975
 
975
976
  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.
976
977
 
978
+ Looking for a starting point? Browse **[`good first issue`](https://github.com/StanislavBG/Content-Grade/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)** — scoped tasks completable in a few hours with no prior codebase knowledge required. Each one has acceptance criteria and a pointer to exactly which file to touch.
979
+
977
980
  ### Roadmap
978
981
 
979
982
  **[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.
@@ -66,7 +66,7 @@ function incrementUsage() {
66
66
  return u.count;
67
67
  }
68
68
 
69
- const FREE_DAILY_LIMIT = 50;
69
+ const FREE_DAILY_LIMIT = 5;
70
70
 
71
71
  function checkDailyLimit() {
72
72
  if (isProUser()) return { ok: true };
@@ -252,20 +252,30 @@ SCORING CALIBRATION:
252
252
 
253
253
  async function cmdAnalyze(filePath) {
254
254
  if (!filePath) {
255
+ if (_demoMode) {
256
+ return cmdDemo();
257
+ }
255
258
  blank();
256
259
  fail(`No file specified.`);
257
260
  blank();
258
- console.log(` Usage:`);
261
+ console.log(` ${B}Usage:${R}`);
259
262
  console.log(` ${CY}content-grade analyze <file>${R} ${D}(or: check <file>)${R}`);
260
263
  blank();
261
- console.log(` Examples:`);
262
- console.log(` ${D}content-grade analyze ./blog-post.md${R}`);
263
- console.log(` ${D}content-grade check ./email-draft.txt${R}`);
264
- console.log(` ${D}content-grade analyze ~/landing-page-copy.md${R}`);
264
+ console.log(` ${B}Examples:${R}`);
265
+ console.log(` ${CY}content-grade analyze ./blog-post.md${R}`);
266
+ console.log(` ${CY}content-grade check ./email-draft.txt${R}`);
267
+ console.log(` ${CY}content-grade analyze ~/landing-page-copy.md${R}`);
265
268
  blank();
266
- console.log(` ${D}No file? Try the demo: ${CY}content-grade demo${R}`);
269
+ console.log(` ${GN}Tip:${R} See it in action first — no file needed:`);
270
+ console.log(` ${CY}content-grade --demo${R} ${D}# live AI analysis on built-in sample${R}`);
271
+ console.log(` ${CY}content-grade demo${R} ${D}# same thing${R}`);
267
272
  blank();
268
- process.exit(2);
273
+ process.exit(1);
274
+ }
275
+
276
+ // If input looks like a URL, delegate to URL analyzer
277
+ if (/^https?:\/\//i.test(filePath)) {
278
+ return cmdAnalyzeUrl(filePath);
269
279
  }
270
280
 
271
281
  const safeFilePath = sanitizeFilePath(filePath);
@@ -273,7 +283,7 @@ async function cmdAnalyze(filePath) {
273
283
  blank();
274
284
  fail(`Invalid file path.`);
275
285
  blank();
276
- process.exit(2);
286
+ process.exit(1);
277
287
  }
278
288
  filePath = safeFilePath;
279
289
 
@@ -285,7 +295,7 @@ async function cmdAnalyze(filePath) {
285
295
  console.log(` ${YL}Check the path and try again.${R}`);
286
296
  console.log(` ${D}Tip: use a relative path like ./my-file.md or an absolute path.${R}`);
287
297
  blank();
288
- process.exit(2);
298
+ process.exit(1);
289
299
  }
290
300
 
291
301
  // Guard: reject directories
@@ -296,7 +306,7 @@ async function cmdAnalyze(filePath) {
296
306
  blank();
297
307
  console.log(` ${D}Pass a text file (.md, .txt) — not a directory.${R}`);
298
308
  blank();
299
- process.exit(2);
309
+ process.exit(1);
300
310
  }
301
311
 
302
312
  // Guard: reject files over 500 KB before reading into memory
@@ -307,7 +317,7 @@ async function cmdAnalyze(filePath) {
307
317
  blank();
308
318
  console.log(` ${YL}Tip:${R} Copy the relevant section into a new file and analyze that.`);
309
319
  blank();
310
- process.exit(2);
320
+ process.exit(1);
311
321
  }
312
322
 
313
323
  const content = readFileSync(absPath, 'utf8');
@@ -320,14 +330,14 @@ async function cmdAnalyze(filePath) {
320
330
  console.log(` ${YL}ContentGrade analyzes written content — blog posts, emails, ad copy, landing pages.${R}`);
321
331
  console.log(` ${D}Supported formats: .md, .txt, .mdx, or any plain-text file.${R}`);
322
332
  blank();
323
- process.exit(2);
333
+ process.exit(1);
324
334
  }
325
335
 
326
336
  if (content.trim().length < 20) {
327
337
  blank();
328
338
  fail(`File is too short to analyze (${content.trim().length} chars). Add some content and try again.`);
329
339
  blank();
330
- process.exit(2);
340
+ process.exit(1);
331
341
  }
332
342
 
333
343
  // Free tier daily limit
@@ -338,9 +348,9 @@ async function cmdAnalyze(filePath) {
338
348
  blank();
339
349
  console.log(` ${B}Options:${R}`);
340
350
  console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
341
- console.log(` ${D}· Unlock 100 checks/day: ${CY}content-grade activate${R}`);
351
+ console.log(` ${D}· Unlock unlimited: ${CY}content-grade activate${R}`);
342
352
  blank();
343
- console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
353
+ console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://buy.stripe.com/5kQeVfew48dT7nf2W48k801${R}`);
344
354
  blank();
345
355
  process.exit(1);
346
356
  }
@@ -491,20 +501,38 @@ async function cmdAnalyze(filePath) {
491
501
  // Track usage
492
502
  incrementUsage();
493
503
 
494
- // Upsell show Pro path after user has seen value
504
+ // Save to file if --save flag is set
505
+ if (_saveMode && result) {
506
+ const defaultSaveName = basename(absPath, extname(absPath)) + '.content-grade.md';
507
+ const outPath = resolve(process.cwd(), _savePath || defaultSaveName);
508
+ try {
509
+ writeFileSync(outPath, analysisToMarkdown(result, basename(absPath), new Date().toISOString().slice(0, 10)), 'utf8');
510
+ blank();
511
+ ok(`Saved to ${outPath}`);
512
+ } catch (saveErr) {
513
+ warn(`Could not save: ${saveErr.message}`);
514
+ }
515
+ }
516
+
517
+ // Next step — graduated CTA after user has seen value
495
518
  blank();
519
+ hr();
496
520
  if (isProUser()) {
497
- console.log(` ${D}Pro active · ${CY}content-grade.github.io/Content-Grade${R}`);
521
+ console.log(` ${D}Pro active · Next: ${CY}content-grade batch ./posts/${R}${D} to analyze a whole directory${R}`);
498
522
  } else {
499
523
  const usage = getUsage();
500
524
  const remaining = Math.max(0, FREE_DAILY_LIMIT - usage.count);
501
- hr();
502
- console.log(` ${MG}${B}Unlock batch mode:${R} ${CY}content-grade activate${R}`);
503
- console.log(` ${D} · Analyze entire directories in one command${R}`);
504
- console.log(` ${D} · 100 checks/day (${remaining} remaining today on free tier)${R}`);
505
- console.log(` ${D} · Get a license at ${CY}content-grade.github.io/Content-Grade${R}`);
525
+ console.log(` ${B}What to do next:${R}`);
506
526
  blank();
507
- console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
527
+ console.log(` ${CY}content-grade headline "Your title here"${R} ${D}# grade a headline in ~5s${R}`);
528
+ console.log(` ${CY}content-grade analyze ./another-post.md${R} ${D}# analyze another file${R}`);
529
+ console.log(` ${CY}content-grade analyze https://yoursite.com/post${R} ${D}# audit any live URL${R}`);
530
+ blank();
531
+ if (remaining <= 10) {
532
+ console.log(` ${YL}${remaining} free checks left today.${R} Unlock unlimited: ${CY}content-grade activate${R}`);
533
+ } else {
534
+ console.log(` ${D}${remaining} free checks left today · Pro ($9/mo): ${CY}content-grade.github.io/Content-Grade${R}`);
535
+ }
508
536
  }
509
537
 
510
538
  // CI exit code — shown after full output so user sees the score before exit
@@ -563,7 +591,7 @@ async function cmdHeadline(text) {
563
591
  console.log(` ${D}content-grade headline "How We Grew From 0 to 10k Users Without Ads"${R}`);
564
592
  console.log(` ${D}content-grade headline "Stop Doing This One Thing in Your Email Subject Lines"${R}`);
565
593
  blank();
566
- process.exit(2);
594
+ process.exit(1);
567
595
  }
568
596
 
569
597
  // Guard: reject oversized input
@@ -574,7 +602,7 @@ async function cmdHeadline(text) {
574
602
  blank();
575
603
  console.log(` ${YL}Tip:${R} Trim your headline to the core message and try again.`);
576
604
  blank();
577
- process.exit(2);
605
+ process.exit(1);
578
606
  }
579
607
 
580
608
  // Free tier daily limit
@@ -585,31 +613,41 @@ async function cmdHeadline(text) {
585
613
  blank();
586
614
  console.log(` ${B}Options:${R}`);
587
615
  console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
588
- console.log(` ${D}· Unlock 100 checks/day: ${CY}content-grade activate${R}`);
616
+ console.log(` ${D}· Unlock unlimited: ${CY}content-grade activate${R}`);
589
617
  blank();
590
- console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
618
+ console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://buy.stripe.com/5kQeVfew48dT7nf2W48k801${R}`);
591
619
  blank();
592
620
  process.exit(1);
593
621
  }
594
622
 
595
- banner();
596
- console.log(` ${B}Grading headline:${R}`);
597
- console.log(` ${D}"${text}"${R}`);
598
- blank();
623
+ if (!_jsonMode && !_quietMode) {
624
+ banner();
625
+ console.log(` ${B}Grading headline:${R}`);
626
+ console.log(` ${D}"${text}"${R}`);
627
+ blank();
628
+ }
599
629
 
600
630
  if (!checkClaude()) {
601
631
  fail(`Claude CLI not found. Run: ${CY}content-grade init${R}`);
602
632
  process.exit(1);
603
633
  }
604
634
 
605
- process.stdout.write(` ${D}Analyzing...${R}`);
635
+ if (!_jsonMode && !_quietMode) process.stdout.write(` ${D}Analyzing...${R}`);
606
636
 
607
637
  let result;
608
638
  try {
609
639
  const raw = await askClaude(`Grade this headline: "${text}"`, HEADLINE_SYSTEM, 'claude-haiku-4-5-20251001');
610
- process.stdout.write(`\r ${GN}✓${R} Done${' '.repeat(20)}\n`);
640
+ if (!_jsonMode && !_quietMode) process.stdout.write(`\r ${GN}✓${R} Done${' '.repeat(20)}\n`);
611
641
  result = parseJSON(raw);
612
642
  recordEvent({ event: 'headline_result', score: result.score });
643
+ if (_jsonMode) {
644
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
645
+ return;
646
+ }
647
+ if (_quietMode) {
648
+ process.stdout.write(`${result.score}\n`);
649
+ return;
650
+ }
613
651
  } catch (err) {
614
652
  process.stdout.write(`\n`);
615
653
  blank();
@@ -667,7 +705,7 @@ async function cmdHeadline(text) {
667
705
  console.log(` ${D}Compare two headlines: ${CY}content-grade start${R} → HeadlineGrader compare${R}`);
668
706
  blank();
669
707
  if (!isProUser()) {
670
- console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
708
+ console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://buy.stripe.com/5kQeVfew48dT7nf2W48k801${R}`);
671
709
  blank();
672
710
  }
673
711
  }
@@ -821,11 +859,11 @@ async function cmdActivate() {
821
859
  blank();
822
860
  fail(`Invalid key — must be at least 8 characters.`);
823
861
  blank();
824
- process.exit(2);
862
+ process.exit(1);
825
863
  }
826
864
 
827
865
  // Validate key against server before storing
828
- const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://contentgrade.ai';
866
+ const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://content-grade.github.io';
829
867
  blank();
830
868
  process.stdout.write(` ${D}Validating key...${R}`);
831
869
 
@@ -842,9 +880,9 @@ async function cmdActivate() {
842
880
  } else {
843
881
  process.stdout.write('\n');
844
882
  blank();
845
- fail(data.message || 'Invalid or expired license key. Visit contentgrade.ai to check your subscription.');
883
+ fail(data.message || 'Invalid or expired license key. Visit content-grade.github.io/Content-Grade to check your subscription.');
846
884
  blank();
847
- process.exit(2);
885
+ process.exit(1);
848
886
  }
849
887
  } catch {
850
888
  // Server unreachable — allow offline activation with a warning
@@ -896,7 +934,7 @@ async function cmdBatch(dirPath) {
896
934
  console.log(` Usage: ${CY}content-grade batch <directory>${R}`);
897
935
  console.log(` Example: ${CY}content-grade batch ./posts${R}`);
898
936
  blank();
899
- process.exit(2);
937
+ process.exit(1);
900
938
  }
901
939
 
902
940
  const safeDirPath = sanitizeFilePath(dirPath);
@@ -904,7 +942,7 @@ async function cmdBatch(dirPath) {
904
942
  blank();
905
943
  fail(`Invalid directory path.`);
906
944
  blank();
907
- process.exit(2);
945
+ process.exit(1);
908
946
  }
909
947
  dirPath = safeDirPath;
910
948
 
@@ -913,7 +951,7 @@ async function cmdBatch(dirPath) {
913
951
  blank();
914
952
  fail(`Directory not found: ${absDir}`);
915
953
  blank();
916
- process.exit(2);
954
+ process.exit(1);
917
955
  }
918
956
 
919
957
  const st = statSync(absDir);
@@ -921,7 +959,7 @@ async function cmdBatch(dirPath) {
921
959
  blank();
922
960
  fail(`${dirPath} is not a directory. Use ${CY}content-grade analyze${R} for single files.`);
923
961
  blank();
924
- process.exit(2);
962
+ process.exit(1);
925
963
  }
926
964
 
927
965
  const files = [];
@@ -1090,7 +1128,7 @@ function cmdStart() {
1090
1128
  info(` EmailForge — ${url}/email-forge`);
1091
1129
  info(` AudienceDecoder — ${url}/audience`);
1092
1130
  blank();
1093
- info(`Free tier: 50 analyses/day. Upgrade at ${url}`);
1131
+ info(`Free tier: 5 analyses/day. Upgrade at ${url}`);
1094
1132
  info(`Press Ctrl+C to stop`);
1095
1133
  blank();
1096
1134
  openBrowser(url);
@@ -1269,10 +1307,11 @@ function cmdHelp() {
1269
1307
  blank();
1270
1308
  console.log(` ${B}QUICK START${R}`);
1271
1309
  blank();
1272
- console.log(` ${CY}npx content-grade headline "Your Headline Here"${R} ${D}# grade a headline (fastest, ~5s)${R}`);
1273
- console.log(` ${CY}npx content-grade analyze ./my-post.md${R} ${D}# full AI content analysis (~20s)${R}`);
1274
- console.log(` ${CY}npx content-grade demo${R} ${D}# live demo on sample content${R}`);
1275
- console.log(` ${CY}npx content-grade start${R} ${D}# launch web dashboard (6 tools)${R}`);
1310
+ console.log(` ${CY}npx content-grade --demo${R} ${D}# live AI demo — no file needed, see it first${R}`);
1311
+ console.log(` ${CY}npx content-grade headline "Your Headline Here"${R} ${D}# grade a headline (fastest, ~5s)${R}`);
1312
+ console.log(` ${CY}npx content-grade analyze ./my-post.md${R} ${D}# full AI analysis of a file (~20s)${R}`);
1313
+ console.log(` ${CY}npx content-grade analyze https://example.com${R} ${D}# audit any live URL (~20s)${R}`);
1314
+ console.log(` ${CY}npx content-grade start${R} ${D}# launch web dashboard (6 tools)${R}`);
1276
1315
  blank();
1277
1316
 
1278
1317
  console.log(` ${B}USAGE${R}`);
@@ -1314,8 +1353,10 @@ function cmdHelp() {
1314
1353
  blank();
1315
1354
  console.log(` ${B}FLAGS${R}`);
1316
1355
  blank();
1356
+ console.log(` ${CY}--demo${R} Run the live AI demo on built-in sample content`);
1317
1357
  console.log(` ${CY}--json${R} Output raw JSON (great for CI pipelines)`);
1318
1358
  console.log(` ${CY}--quiet${R} Output score number only (for scripting)`);
1359
+ console.log(` ${CY}--save [file]${R} Save analysis to a markdown file (default: <input>.content-grade.md)`);
1319
1360
  console.log(` ${CY}--ci${R} Exit 0 if score passes threshold, exit 1 if it fails`);
1320
1361
  console.log(` ${CY}--threshold <n>${R} Score threshold for --ci mode (default: 60)`);
1321
1362
  console.log(` ${CY}--verbose${R} Show debug info: model, timing, raw response length`);
@@ -1456,18 +1497,29 @@ function cmdQuickDemo() {
1456
1497
  hr();
1457
1498
  console.log(` ${D}↑ This is a ${B}sample${R}${D} — your real content gets a live AI analysis in ~20 seconds.${R}`);
1458
1499
  blank();
1459
- console.log(` ${B}${CY}Try it now:${R}`);
1460
- blank();
1461
- console.log(` ${CY}npx content-grade headline "Your headline here"${R} ${D}# fastest — grade any headline${R}`);
1462
- console.log(` ${CY}npx content-grade analyze ./your-post.md${R} ${D}# full AI analysis of a file${R}`);
1463
- console.log(` ${CY}npx content-grade demo${R} ${D}# live demo — takes ~20s with Claude${R}`);
1464
- blank();
1465
-
1500
+ hr();
1466
1501
  if (isFirstRun()) {
1467
- console.log(` ${D}Requires Claude CLI (free): ${CY}claude.ai/code${R}${D} install ${CY}claude login${R}`);
1468
- console.log(` ${D}Setup check: ${CY}npx content-grade init${R}`);
1502
+ console.log(` ${B}${CY}Try it on real content — 3 ways to start:${R}`);
1503
+ blank();
1504
+ console.log(` ${B}1.${R} Grade a headline ${D}(~5 seconds, works right now)${R}`);
1505
+ console.log(` ${CY}npx content-grade headline "Your headline here"${R}`);
1506
+ blank();
1507
+ console.log(` ${B}2.${R} Run the live AI demo ${D}(full analysis on sample content)${R}`);
1508
+ console.log(` ${CY}npx content-grade --demo${R}`);
1509
+ blank();
1510
+ console.log(` ${B}3.${R} Analyze your own file`);
1511
+ console.log(` ${CY}npx content-grade analyze ./your-post.md${R}`);
1469
1512
  blank();
1513
+ console.log(` ${D}All three require Claude CLI (free): ${CY}claude.ai/code${R}${D} → install → ${CY}claude login${R}`);
1514
+ console.log(` ${D}First time? Run: ${CY}npx content-grade init${R}${D} to verify setup.${R}`);
1515
+ } else {
1516
+ console.log(` ${B}${CY}Grade your own content:${R}`);
1517
+ blank();
1518
+ console.log(` ${CY}npx content-grade --demo${R} ${D}# live AI demo${R}`);
1519
+ console.log(` ${CY}npx content-grade headline "Your headline here"${R} ${D}# ~5 seconds${R}`);
1520
+ console.log(` ${CY}npx content-grade analyze ./your-post.md${R} ${D}# full analysis${R}`);
1470
1521
  }
1522
+ blank();
1471
1523
  }
1472
1524
 
1473
1525
  // ── Demo command ──────────────────────────────────────────────────────────────
@@ -1585,10 +1637,10 @@ async function cmdDemo() {
1585
1637
 
1586
1638
  // ── URL fetcher ───────────────────────────────────────────────────────────────
1587
1639
 
1588
- function fetchUrl(url) {
1640
+ function fetchUrl(url, { rejectUnauthorized = true } = {}) {
1589
1641
  return new Promise((resolve, reject) => {
1590
1642
  const get = url.startsWith('https') ? httpsGet : httpGet;
1591
- const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/content-grade/Content-Grade)' }, timeout: 15000 }, (res) => {
1643
+ const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/content-grade/Content-Grade)' }, timeout: 15000, rejectUnauthorized }, (res) => {
1592
1644
  // Follow one redirect — validate location is http/https before following
1593
1645
  if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
1594
1646
  const loc = res.headers.location;
@@ -1597,7 +1649,7 @@ function fetchUrl(url) {
1597
1649
  res.resume();
1598
1650
  return;
1599
1651
  }
1600
- fetchUrl(loc).then(resolve).catch(reject);
1652
+ fetchUrl(loc, { rejectUnauthorized }).then(resolve).catch(reject);
1601
1653
  res.resume();
1602
1654
  return;
1603
1655
  }
@@ -1611,7 +1663,16 @@ function fetchUrl(url) {
1611
1663
  res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8', 0, 500000)));
1612
1664
  res.on('error', reject);
1613
1665
  });
1614
- req.on('error', reject);
1666
+ req.on('error', (err) => {
1667
+ // TLS cert failures: retry without verification (dev tool — user may be auditing
1668
+ // internal sites with self-signed certs or proxies that intercept TLS)
1669
+ const isCertErr = err.code && /CERT|SELF_SIGNED|UNABLE_TO/i.test(err.code);
1670
+ if (isCertErr && rejectUnauthorized) {
1671
+ fetchUrl(url, { rejectUnauthorized: false }).then(resolve).catch(reject);
1672
+ return;
1673
+ }
1674
+ reject(err);
1675
+ });
1615
1676
  req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
1616
1677
  });
1617
1678
  }
@@ -1641,13 +1702,13 @@ async function cmdAnalyzeUrl(url) {
1641
1702
  blank();
1642
1703
  fail(`Invalid URL: must start with http:// or https://`);
1643
1704
  blank();
1644
- process.exit(2);
1705
+ process.exit(1);
1645
1706
  }
1646
1707
  if (sanitizedUrl.length > 2048) {
1647
1708
  blank();
1648
1709
  fail(`URL too long (${sanitizedUrl.length} chars). Maximum is 2048.`);
1649
1710
  blank();
1650
- process.exit(2);
1711
+ process.exit(1);
1651
1712
  }
1652
1713
  url = sanitizedUrl;
1653
1714
 
@@ -1746,6 +1807,78 @@ function findBestContentFile(dirPath) {
1746
1807
  return scored[0].path;
1747
1808
  }
1748
1809
 
1810
+ // ── Save analysis to markdown ─────────────────────────────────────────────────
1811
+
1812
+ function analysisToMarkdown(result, sourceName, analyzedAt) {
1813
+ const grade = result.grade ?? gradeLetter(result.total_score).replace(/\x1b\[[0-9;]*m/g, '');
1814
+ const lines = [];
1815
+ lines.push(`# ContentGrade Report: ${sourceName}`);
1816
+ lines.push(``);
1817
+ lines.push(`Analyzed: ${analyzedAt} `);
1818
+ lines.push(`Overall score: **${result.total_score}/100 (${grade})** `);
1819
+ if (result.content_type) lines.push(`Content type: ${result.content_type.replace('_', ' ')}`);
1820
+ lines.push(``);
1821
+
1822
+ if (result.headline?.text) {
1823
+ lines.push(`## Headline`);
1824
+ lines.push(``);
1825
+ lines.push(`> ${result.headline.text}`);
1826
+ lines.push(``);
1827
+ lines.push(`Score: ${result.headline.score}/100`);
1828
+ if (result.headline.feedback) lines.push(`${result.headline.feedback}`);
1829
+ lines.push(``);
1830
+ }
1831
+
1832
+ if (result.one_line_verdict) {
1833
+ lines.push(`## Verdict`);
1834
+ lines.push(``);
1835
+ lines.push(`${result.one_line_verdict}`);
1836
+ lines.push(``);
1837
+ }
1838
+
1839
+ if (result.dimensions) {
1840
+ lines.push(`## Dimension Breakdown`);
1841
+ lines.push(``);
1842
+ for (const [, dim] of Object.entries(result.dimensions)) {
1843
+ lines.push(`**${dim.label}**: ${dim.score}/100`);
1844
+ if (dim.feedback) lines.push(`${dim.feedback}`);
1845
+ lines.push(``);
1846
+ }
1847
+ }
1848
+
1849
+ if (result.strengths?.length) {
1850
+ lines.push(`## Strengths`);
1851
+ lines.push(``);
1852
+ for (const s of result.strengths) lines.push(`- ${s}`);
1853
+ lines.push(``);
1854
+ }
1855
+
1856
+ if (result.improvements?.length) {
1857
+ lines.push(`## Top Improvements`);
1858
+ lines.push(``);
1859
+ for (const imp of result.improvements) {
1860
+ const pri = imp.priority === 'high' ? '🔴' : '🟡';
1861
+ lines.push(`### ${pri} ${imp.issue}`);
1862
+ lines.push(``);
1863
+ lines.push(`**Fix:** ${imp.fix}`);
1864
+ lines.push(``);
1865
+ }
1866
+ }
1867
+
1868
+ if (result.headline_rewrites?.length) {
1869
+ lines.push(`## Headline Rewrites`);
1870
+ lines.push(``);
1871
+ for (let i = 0; i < result.headline_rewrites.length; i++) {
1872
+ lines.push(`${i + 1}. ${result.headline_rewrites[i]}`);
1873
+ }
1874
+ lines.push(``);
1875
+ }
1876
+
1877
+ lines.push(`---`);
1878
+ lines.push(`Generated by [ContentGrade](https://buy.stripe.com/5kQeVfew48dT7nf2W48k801) v${_version}`);
1879
+ return lines.join('\n');
1880
+ }
1881
+
1749
1882
  // ── Router ────────────────────────────────────────────────────────────────────
1750
1883
 
1751
1884
  const _rawArgs = process.argv.slice(2);
@@ -1757,10 +1890,17 @@ const _threshIdx = _rawArgs.indexOf('--threshold');
1757
1890
  const _ciThreshold = (_threshIdx !== -1 && _rawArgs[_threshIdx + 1])
1758
1891
  ? (parseInt(_rawArgs[_threshIdx + 1], 10) || 60)
1759
1892
  : 60;
1893
+ const _saveIdx = _rawArgs.indexOf('--save');
1894
+ const _saveMode = _saveIdx !== -1;
1895
+ const _savePath = (_saveMode && _rawArgs[_saveIdx + 1] && !_rawArgs[_saveIdx + 1].startsWith('-'))
1896
+ ? _rawArgs[_saveIdx + 1]
1897
+ : null;
1898
+ const _demoMode = _rawArgs.includes('--demo');
1760
1899
  const args = _rawArgs.filter((a, i) => {
1761
- if (['--no-telemetry', '--json', '--quiet', '--verbose', '--ci'].includes(a)) return false;
1900
+ if (['--no-telemetry', '--json', '--quiet', '--verbose', '--ci', '--save', '--demo'].includes(a)) return false;
1762
1901
  if (a === '--threshold') return false;
1763
1902
  if (i > 0 && _rawArgs[i - 1] === '--threshold') return false;
1903
+ if (_saveMode && i > 0 && _rawArgs[i - 1] === '--save' && !a.startsWith('-')) return false;
1764
1904
  return true;
1765
1905
  });
1766
1906
  const raw = args[0];
@@ -1779,7 +1919,119 @@ function looksLikePath(s) {
1779
1919
  try { statSync(resolve(process.cwd(), s)); return true; } catch { return false; }
1780
1920
  }
1781
1921
 
1782
- switch (cmd) {
1922
+ // Per-command --help: "content-grade <cmd> --help" → command-specific help
1923
+ if (_rawArgs.includes('--help') || _rawArgs.includes('-h')) {
1924
+ switch (cmd) {
1925
+ case 'analyze':
1926
+ case 'analyse':
1927
+ case 'check':
1928
+ case 'grade':
1929
+ blank();
1930
+ console.log(` ${B}content-grade analyze <file>${R} ${D}(aliases: grade, check)${R}`);
1931
+ blank();
1932
+ console.log(` Analyzes written content and returns an AI-powered score (0–100) with`);
1933
+ console.log(` dimension breakdown, specific improvements, and headline rewrites.`);
1934
+ blank();
1935
+ console.log(` ${B}Arguments:${R}`);
1936
+ console.log(` ${CY}<file>${R} Path to a .md, .txt, or .mdx file`);
1937
+ blank();
1938
+ console.log(` ${B}Flags:${R}`);
1939
+ console.log(` ${CY}--demo${R} Run on built-in sample content (no file needed)`);
1940
+ console.log(` ${CY}--json${R} Output raw JSON (great for CI pipelines)`);
1941
+ console.log(` ${CY}--quiet${R} Output score number only (for scripting)`);
1942
+ console.log(` ${CY}--ci${R} Exit 0 if score passes, 1 if it fails`);
1943
+ console.log(` ${CY}--threshold <n>${R} Score threshold for --ci (default: 60)`);
1944
+ console.log(` ${CY}--verbose${R} Show debug info: model, timing, response length`);
1945
+ blank();
1946
+ console.log(` ${B}Examples:${R}`);
1947
+ console.log(` ${CY}content-grade analyze --demo${R} ${D}# live demo on sample content${R}`);
1948
+ console.log(` ${CY}content-grade analyze ./blog-post.md${R}`);
1949
+ console.log(` ${CY}content-grade analyze ./readme.md --json${R}`);
1950
+ console.log(` ${CY}content-grade analyze ./copy.md --ci --threshold 70${R}`);
1951
+ console.log(` ${D}# store score in a variable${R}`);
1952
+ console.log(` ${CY}score=\$(content-grade analyze ./post.md --quiet)${R}`);
1953
+ blank();
1954
+ process.exit(0);
1955
+ break;
1956
+ case 'headline':
1957
+ blank();
1958
+ console.log(` ${B}content-grade headline "<text>"${R}`);
1959
+ blank();
1960
+ console.log(` Grades a single headline using 4 direct response copywriting frameworks:`);
1961
+ console.log(` Rule of One, Value Equation, Readability, and Proof/Promise.`);
1962
+ console.log(` Returns a 0–100 score, verdict, and 2 stronger rewrites. Takes ~5 seconds.`);
1963
+ blank();
1964
+ console.log(` ${B}Arguments:${R}`);
1965
+ console.log(` ${CY}"<text>"${R} The headline to grade (quote it to avoid shell expansion)`);
1966
+ blank();
1967
+ console.log(` ${B}Flags:${R}`);
1968
+ console.log(` ${CY}--json${R} Output raw JSON`);
1969
+ console.log(` ${CY}--quiet${R} Output score number only`);
1970
+ console.log(` ${CY}--verbose${R} Show debug info`);
1971
+ blank();
1972
+ console.log(` ${B}Examples:${R}`);
1973
+ console.log(` ${CY}content-grade headline "The 5-Minute Fix That Doubles Landing Page Conversions"${R}`);
1974
+ console.log(` ${CY}content-grade headline "How We Grew From 0 to 10k Users" --json${R}`);
1975
+ blank();
1976
+ process.exit(0);
1977
+ break;
1978
+ case 'seo-audit':
1979
+ case 'seoaudit':
1980
+ blank();
1981
+ console.log(` ${B}content-grade seo-audit <url>${R}`);
1982
+ blank();
1983
+ console.log(` Fetches a live URL, extracts readable content, and runs the same full AI`);
1984
+ console.log(` analysis as the analyze command. Good for auditing competitors or your own pages.`);
1985
+ blank();
1986
+ console.log(` ${B}Arguments:${R}`);
1987
+ console.log(` ${CY}<url>${R} Full URL starting with http:// or https://`);
1988
+ blank();
1989
+ console.log(` ${B}Flags:${R}`);
1990
+ console.log(` ${CY}--json${R} Output raw JSON`);
1991
+ console.log(` ${CY}--quiet${R} Output score number only`);
1992
+ console.log(` ${CY}--ci${R} Exit 0/1 based on score threshold`);
1993
+ console.log(` ${CY}--threshold <n>${R} Score threshold for --ci (default: 60)`);
1994
+ blank();
1995
+ console.log(` ${B}Examples:${R}`);
1996
+ console.log(` ${CY}content-grade seo-audit https://yoursite.com/blog-post${R}`);
1997
+ console.log(` ${CY}content-grade seo-audit https://example.com --json${R}`);
1998
+ blank();
1999
+ process.exit(0);
2000
+ break;
2001
+ case 'batch':
2002
+ blank();
2003
+ console.log(` ${B}content-grade batch <directory>${R} ${MG}[Pro]${R}`);
2004
+ blank();
2005
+ console.log(` Analyzes all .md, .txt, and .mdx files in a directory and prints a sorted`);
2006
+ console.log(` summary with scores. Requires a Pro license.`);
2007
+ blank();
2008
+ console.log(` ${B}Arguments:${R}`);
2009
+ console.log(` ${CY}<directory>${R} Path to a directory containing content files`);
2010
+ blank();
2011
+ console.log(` ${B}Flags:${R}`);
2012
+ console.log(` ${CY}--json${R} Output results as a JSON array (replaces styled output)`);
2013
+ blank();
2014
+ console.log(` ${B}Examples:${R}`);
2015
+ console.log(` ${CY}content-grade batch ./posts${R}`);
2016
+ console.log(` ${CY}content-grade batch ./content --json${R}`);
2017
+ blank();
2018
+ process.exit(0);
2019
+ break;
2020
+ default:
2021
+ cmdHelp();
2022
+ process.exit(0);
2023
+ }
2024
+ }
2025
+
2026
+ if (_demoMode) {
2027
+ recordEvent({ event: 'command', command: 'demo' });
2028
+ cmdDemo().catch(err => {
2029
+ blank();
2030
+ fail(`Demo error: ${err.message}`);
2031
+ blank();
2032
+ process.exit(1);
2033
+ });
2034
+ } else switch (cmd) {
1783
2035
  case 'analyze':
1784
2036
  case 'analyse':
1785
2037
  case 'check':
@@ -1851,7 +2103,7 @@ switch (cmd) {
1851
2103
  console.log(` ${B}Usage:${R} ${CY}content-grade seo-audit <url>${R}`);
1852
2104
  console.log(` ${B}Example:${R} ${CY}content-grade seo-audit https://yoursite.com/blog-post${R}`);
1853
2105
  blank();
1854
- process.exit(2);
2106
+ process.exit(1);
1855
2107
  }
1856
2108
  cmdAnalyzeUrl(args[1]).catch(err => {
1857
2109
  blank();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-grade",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
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": {