content-grade 1.0.11 → 1.0.13

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
@@ -2,13 +2,14 @@
2
2
 
3
3
  > AI-powered content quality scoring for developers and content teams. Score any blog post, landing page, ad copy, or email — in under 30 seconds. No API keys. No accounts. Runs on your local Claude CLI.
4
4
 
5
+ [![CI](https://github.com/StanislavBG/Content-Grade/actions/workflows/ci.yml/badge.svg)](https://github.com/StanislavBG/Content-Grade/actions/workflows/ci.yml)
5
6
  [![npm](https://img.shields.io/npm/v/content-grade.svg)](https://www.npmjs.com/package/content-grade)
6
7
  [![npm downloads/month](https://img.shields.io/npm/dm/content-grade.svg)](https://www.npmjs.com/package/content-grade)
7
8
  [![npm downloads/week](https://img.shields.io/npm/dw/content-grade.svg)](https://www.npmjs.com/package/content-grade)
8
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
10
  [![Node.js 18+](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
10
11
  [![Requires Claude CLI](https://img.shields.io/badge/requires-Claude%20CLI-orange)](https://claude.ai/code)
11
- [![GitHub Discussions](https://img.shields.io/badge/GitHub-Discussions-blue?logo=github)](https://github.com/content-grade/Content-Grade/discussions)
12
+ [![GitHub Discussions](https://img.shields.io/badge/GitHub-Discussions-blue?logo=github)](https://github.com/StanislavBG/Content-Grade/discussions)
12
13
  [![Early Adopter seats open](https://img.shields.io/badge/Early%20Adopter-50%20seats%20open-success)](EARLY_ADOPTERS.md)
13
14
 
14
15
  ---
@@ -171,7 +172,7 @@ Launch with `content-grade start` — opens at [http://localhost:4000](http://lo
171
172
  | **EmailForge** | `/email-forge` | Subject line + body copy for click-through optimization |
172
173
  | **AudienceDecoder** | `/audience` | Twitter handle → audience archetypes and content patterns |
173
174
 
174
- Free tier: **50 analyses/day per tool**. Pro ($9/mo): **100 analyses/day** + all tools.
175
+ Free tier: **50 analyses/day per tool**. Pro ($19/mo): **100 analyses/day** + all tools.
175
176
 
176
177
  ---
177
178
 
@@ -515,7 +516,7 @@ Stripe variables are entirely optional — all tools work without them; upgrade
515
516
  ## Self-hosting / Development
516
517
 
517
518
  ```bash
518
- git clone https://github.com/content-grade/Content-Grade
519
+ git clone https://github.com/StanislavBG/Content-Grade
519
520
  cd Content-Grade
520
521
  npm install
521
522
  npm run dev # hot reload: server at :4000, client at :3000
@@ -912,7 +913,7 @@ echo "$DATE,$HEADLINE,$SCORE" >> headline-scores.csv
912
913
 
913
914
  | | Free | Pro |
914
915
  |-|------|-----|
915
- | Price | Free forever | $9/month |
916
+ | Price | Free forever | $19/month |
916
917
  | Analyses/day (CLI) | Unlimited | Unlimited |
917
918
  | Analyses/day (web dashboard, per tool) | 3 | 100 |
918
919
  | HeadlineGrader (single grade) | ✓ | ✓ |
@@ -945,25 +946,25 @@ ContentGrade is built in public. The community is the roadmap.
945
946
 
946
947
  ### GitHub Discussions
947
948
 
948
- **[Join the conversation →](https://github.com/content-grade/Content-Grade/discussions)**
949
+ **[Join the conversation →](https://github.com/StanislavBG/Content-Grade/discussions)**
949
950
 
950
951
  | Category | What it's for |
951
952
  |----------|--------------|
952
- | [Q&A](https://github.com/content-grade/Content-Grade/discussions/new?category=q-a) | "Why is my score lower than expected?" — questions get answered here |
953
- | [Show & Tell](https://github.com/content-grade/Content-Grade/discussions/new?category=show-and-tell) | Share your workflow, integration, or results — early adopters post here |
954
- | [Ideas](https://github.com/content-grade/Content-Grade/discussions/new?category=ideas) | Feature requests and suggestions before they become issues |
953
+ | [Q&A](https://github.com/StanislavBG/Content-Grade/discussions/new?category=q-a) | "Why is my score lower than expected?" — questions get answered here |
954
+ | [Show & Tell](https://github.com/StanislavBG/Content-Grade/discussions/new?category=show-and-tell) | Share your workflow, integration, or results — early adopters post here |
955
+ | [Ideas](https://github.com/StanislavBG/Content-Grade/discussions/new?category=ideas) | Feature requests and suggestions before they become issues |
955
956
 
956
957
  ### Early Adopter Program — 50 seats
957
958
 
958
959
  **[EARLY_ADOPTERS.md](EARLY_ADOPTERS.md)** — The first 50 users who install and share feedback get:
959
960
 
960
- - **Pro tier free for 12 months** ($9/mo value)
961
+ - **Pro tier free for 12 months** ($19/mo value)
961
962
  - Direct feedback channel with the maintainer
962
963
  - Name in CONTRIBUTORS.md (permanent)
963
964
  - Roadmap preview + design input before features ship
964
965
  - 30-minute onboarding call (optional)
965
966
 
966
- One action to claim: run ContentGrade, then post in [Show & Tell](https://github.com/content-grade/Content-Grade/discussions/new?category=show-and-tell) with `[Early Adopter]` in the title.
967
+ One action to claim: run ContentGrade, then post in [Show & Tell](https://github.com/StanislavBG/Content-Grade/discussions/new?category=show-and-tell) with `[Early Adopter]` in the title.
967
968
 
968
969
  ### Champions Program
969
970
 
@@ -973,18 +974,24 @@ Power users who go beyond early adoption get early access to `@beta` releases, a
973
974
 
974
975
  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.
975
976
 
977
+ 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.
978
+
976
979
  ### Roadmap
977
980
 
978
- **[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/content-grade/Content-Grade/discussions/new?category=ideas) to challenge or extend it.
981
+ **[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.
979
982
 
980
983
  ### Feedback Loop
981
984
 
982
- **[FEEDBACK_LOOP.md](FEEDBACK_LOOP.md)** — how feedback flows into product decisions. Monthly structured check-ins, 48h triage SLA, and roadmap sync. Use the [monthly feedback template](https://github.com/content-grade/Content-Grade/issues/new?template=monthly_feedback.yml) to share your experience.
985
+ **[FEEDBACK_LOOP.md](FEEDBACK_LOOP.md)** — how feedback flows into product decisions. Monthly structured check-ins, 48h triage SLA, and roadmap sync. Use the [monthly feedback template](https://github.com/StanislavBG/Content-Grade/issues/new?template=monthly_feedback.yml) to share your experience.
983
986
 
984
987
  ### Contributors
985
988
 
986
989
  **[CONTRIBUTORS.md](CONTRIBUTORS.md)** — everyone who has improved ContentGrade. Code, bug reports, and real-world feedback all count. Early Adopters and Champions are listed here permanently.
987
990
 
991
+ ### How did you find ContentGrade?
992
+
993
+ If you found this tool useful, we'd love to know where you came across it — a Reddit thread, a blog post, a colleague's recommendation? Drop a note in [GitHub Discussions → Q&A](https://github.com/Content-Grade/Content-Grade/discussions/new?category=q-a) with the subject "How I found ContentGrade". This is 100% optional and takes 30 seconds. It helps us understand which communities to focus on and which content resonates.
994
+
988
995
  ### Use ContentGrade in your project
989
996
 
990
997
  Add a badge to show your content meets the bar:
@@ -999,6 +1006,11 @@ See [docs/social-proof/built-with-badge.md](docs/social-proof/built-with-badge.m
999
1006
 
1000
1007
  ---
1001
1008
 
1009
+ ## Legal
1010
+
1011
+ - [Privacy Policy](https://content-grade.github.io/Content-Grade/privacy.html)
1012
+ - [Terms of Service](https://content-grade.github.io/Content-Grade/terms.html)
1013
+
1002
1014
  ## License
1003
1015
 
1004
1016
  MIT
@@ -265,7 +265,12 @@ async function cmdAnalyze(filePath) {
265
265
  blank();
266
266
  console.log(` ${D}No file? Try the demo: ${CY}content-grade demo${R}`);
267
267
  blank();
268
- process.exit(2);
268
+ process.exit(1);
269
+ }
270
+
271
+ // If input looks like a URL, delegate to URL analyzer
272
+ if (/^https?:\/\//i.test(filePath)) {
273
+ return cmdAnalyzeUrl(filePath);
269
274
  }
270
275
 
271
276
  const safeFilePath = sanitizeFilePath(filePath);
@@ -273,7 +278,7 @@ async function cmdAnalyze(filePath) {
273
278
  blank();
274
279
  fail(`Invalid file path.`);
275
280
  blank();
276
- process.exit(2);
281
+ process.exit(1);
277
282
  }
278
283
  filePath = safeFilePath;
279
284
 
@@ -285,7 +290,7 @@ async function cmdAnalyze(filePath) {
285
290
  console.log(` ${YL}Check the path and try again.${R}`);
286
291
  console.log(` ${D}Tip: use a relative path like ./my-file.md or an absolute path.${R}`);
287
292
  blank();
288
- process.exit(2);
293
+ process.exit(1);
289
294
  }
290
295
 
291
296
  // Guard: reject directories
@@ -296,7 +301,7 @@ async function cmdAnalyze(filePath) {
296
301
  blank();
297
302
  console.log(` ${D}Pass a text file (.md, .txt) — not a directory.${R}`);
298
303
  blank();
299
- process.exit(2);
304
+ process.exit(1);
300
305
  }
301
306
 
302
307
  // Guard: reject files over 500 KB before reading into memory
@@ -307,7 +312,7 @@ async function cmdAnalyze(filePath) {
307
312
  blank();
308
313
  console.log(` ${YL}Tip:${R} Copy the relevant section into a new file and analyze that.`);
309
314
  blank();
310
- process.exit(2);
315
+ process.exit(1);
311
316
  }
312
317
 
313
318
  const content = readFileSync(absPath, 'utf8');
@@ -320,14 +325,14 @@ async function cmdAnalyze(filePath) {
320
325
  console.log(` ${YL}ContentGrade analyzes written content — blog posts, emails, ad copy, landing pages.${R}`);
321
326
  console.log(` ${D}Supported formats: .md, .txt, .mdx, or any plain-text file.${R}`);
322
327
  blank();
323
- process.exit(2);
328
+ process.exit(1);
324
329
  }
325
330
 
326
331
  if (content.trim().length < 20) {
327
332
  blank();
328
333
  fail(`File is too short to analyze (${content.trim().length} chars). Add some content and try again.`);
329
334
  blank();
330
- process.exit(2);
335
+ process.exit(1);
331
336
  }
332
337
 
333
338
  // Free tier daily limit
@@ -340,7 +345,7 @@ async function cmdAnalyze(filePath) {
340
345
  console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
341
346
  console.log(` ${D}· Unlock 100 checks/day: ${CY}content-grade activate${R}`);
342
347
  blank();
343
- console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
348
+ console.log(` ${MG}Upgrade to Pro ($9/mo) for 100 analyses/day →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
344
349
  blank();
345
350
  process.exit(1);
346
351
  }
@@ -388,8 +393,16 @@ async function cmdAnalyze(filePath) {
388
393
  result = parseJSON(raw);
389
394
  recordEvent({ event: 'analyze_result', score: result.total_score, content_type: result.content_type });
390
395
  // Machine-readable output modes (exit cleanly, skip styled output)
391
- if (_jsonMode) { process.stdout.write(JSON.stringify(result, null, 2) + '\n'); return; }
392
- if (_quietMode) { process.stdout.write(`${result.total_score}\n`); return; }
396
+ if (_jsonMode) {
397
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
398
+ if (_ciMode) process.exit(result.total_score >= _ciThreshold ? 0 : 1);
399
+ return;
400
+ }
401
+ if (_quietMode) {
402
+ process.stdout.write(`${result.total_score}\n`);
403
+ if (_ciMode) process.exit(result.total_score >= _ciThreshold ? 0 : 1);
404
+ return;
405
+ }
393
406
  } catch (err) {
394
407
  process.stdout.write(`\n`);
395
408
  blank();
@@ -483,21 +496,54 @@ async function cmdAnalyze(filePath) {
483
496
  // Track usage
484
497
  incrementUsage();
485
498
 
486
- // Upsell show Pro path after user has seen value
499
+ // Save to file if --save flag is set
500
+ if (_saveMode && result) {
501
+ const defaultSaveName = basename(absPath, extname(absPath)) + '.content-grade.md';
502
+ const outPath = resolve(process.cwd(), _savePath || defaultSaveName);
503
+ try {
504
+ writeFileSync(outPath, analysisToMarkdown(result, basename(absPath), new Date().toISOString().slice(0, 10)), 'utf8');
505
+ blank();
506
+ ok(`Saved to ${outPath}`);
507
+ } catch (saveErr) {
508
+ warn(`Could not save: ${saveErr.message}`);
509
+ }
510
+ }
511
+
512
+ // Next step — graduated CTA after user has seen value
487
513
  blank();
514
+ hr();
488
515
  if (isProUser()) {
489
- console.log(` ${D}Pro active · ${CY}content-grade.github.io/Content-Grade${R}`);
516
+ console.log(` ${D}Pro active · Next: ${CY}content-grade batch ./posts/${R}${D} to analyze a whole directory${R}`);
490
517
  } else {
491
518
  const usage = getUsage();
492
519
  const remaining = Math.max(0, FREE_DAILY_LIMIT - usage.count);
520
+ console.log(` ${B}What to do next:${R}`);
521
+ blank();
522
+ console.log(` ${CY}content-grade headline "Your title here"${R} ${D}# grade a headline in ~5s${R}`);
523
+ console.log(` ${CY}content-grade analyze ./another-post.md${R} ${D}# analyze another file${R}`);
524
+ console.log(` ${CY}content-grade analyze https://yoursite.com/post${R} ${D}# audit any live URL${R}`);
525
+ blank();
526
+ if (remaining <= 10) {
527
+ console.log(` ${YL}${remaining} free checks left today.${R} Unlock 100/day: ${CY}content-grade activate${R}`);
528
+ } else {
529
+ console.log(` ${D}${remaining} free checks left today · Pro ($9/mo): ${CY}content-grade.github.io/Content-Grade${R}`);
530
+ }
531
+ }
532
+
533
+ // CI exit code — shown after full output so user sees the score before exit
534
+ if (_ciMode) {
535
+ const passed = result.total_score >= _ciThreshold;
536
+ blank();
493
537
  hr();
494
- console.log(` ${MG}${B}Unlock batch mode:${R} ${CY}content-grade activate${R}`);
495
- console.log(` ${D} · Analyze entire directories in one command${R}`);
496
- console.log(` ${D} · 100 checks/day (${remaining} remaining today on free tier)${R}`);
497
- console.log(` ${D} · Get a license at ${CY}content-grade.github.io/Content-Grade${R}`);
538
+ if (passed) {
539
+ ok(`CI PASS — score ${result.total_score} meets threshold ${_ciThreshold}`);
540
+ } else {
541
+ fail(`CI FAIL — score ${result.total_score} is below threshold ${_ciThreshold}`);
542
+ }
498
543
  blank();
499
- console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
544
+ process.exit(passed ? 0 : 1);
500
545
  }
546
+
501
547
  blank();
502
548
  }
503
549
 
@@ -540,7 +586,7 @@ async function cmdHeadline(text) {
540
586
  console.log(` ${D}content-grade headline "How We Grew From 0 to 10k Users Without Ads"${R}`);
541
587
  console.log(` ${D}content-grade headline "Stop Doing This One Thing in Your Email Subject Lines"${R}`);
542
588
  blank();
543
- process.exit(2);
589
+ process.exit(1);
544
590
  }
545
591
 
546
592
  // Guard: reject oversized input
@@ -551,7 +597,7 @@ async function cmdHeadline(text) {
551
597
  blank();
552
598
  console.log(` ${YL}Tip:${R} Trim your headline to the core message and try again.`);
553
599
  blank();
554
- process.exit(2);
600
+ process.exit(1);
555
601
  }
556
602
 
557
603
  // Free tier daily limit
@@ -564,29 +610,39 @@ async function cmdHeadline(text) {
564
610
  console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
565
611
  console.log(` ${D}· Unlock 100 checks/day: ${CY}content-grade activate${R}`);
566
612
  blank();
567
- console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
613
+ console.log(` ${MG}Upgrade to Pro ($9/mo) for 100 analyses/day →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
568
614
  blank();
569
615
  process.exit(1);
570
616
  }
571
617
 
572
- banner();
573
- console.log(` ${B}Grading headline:${R}`);
574
- console.log(` ${D}"${text}"${R}`);
575
- blank();
618
+ if (!_jsonMode && !_quietMode) {
619
+ banner();
620
+ console.log(` ${B}Grading headline:${R}`);
621
+ console.log(` ${D}"${text}"${R}`);
622
+ blank();
623
+ }
576
624
 
577
625
  if (!checkClaude()) {
578
626
  fail(`Claude CLI not found. Run: ${CY}content-grade init${R}`);
579
627
  process.exit(1);
580
628
  }
581
629
 
582
- process.stdout.write(` ${D}Analyzing...${R}`);
630
+ if (!_jsonMode && !_quietMode) process.stdout.write(` ${D}Analyzing...${R}`);
583
631
 
584
632
  let result;
585
633
  try {
586
634
  const raw = await askClaude(`Grade this headline: "${text}"`, HEADLINE_SYSTEM, 'claude-haiku-4-5-20251001');
587
- process.stdout.write(`\r ${GN}✓${R} Done${' '.repeat(20)}\n`);
635
+ if (!_jsonMode && !_quietMode) process.stdout.write(`\r ${GN}✓${R} Done${' '.repeat(20)}\n`);
588
636
  result = parseJSON(raw);
589
637
  recordEvent({ event: 'headline_result', score: result.score });
638
+ if (_jsonMode) {
639
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
640
+ return;
641
+ }
642
+ if (_quietMode) {
643
+ process.stdout.write(`${result.score}\n`);
644
+ return;
645
+ }
590
646
  } catch (err) {
591
647
  process.stdout.write(`\n`);
592
648
  blank();
@@ -644,7 +700,7 @@ async function cmdHeadline(text) {
644
700
  console.log(` ${D}Compare two headlines: ${CY}content-grade start${R} → HeadlineGrader compare${R}`);
645
701
  blank();
646
702
  if (!isProUser()) {
647
- console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
703
+ console.log(` ${MG}Upgrade to Pro ($9/mo) for 100 analyses/day →${R} ${CY}https://content-grade.github.io/Content-Grade/${R}`);
648
704
  blank();
649
705
  }
650
706
  }
@@ -798,11 +854,11 @@ async function cmdActivate() {
798
854
  blank();
799
855
  fail(`Invalid key — must be at least 8 characters.`);
800
856
  blank();
801
- process.exit(2);
857
+ process.exit(1);
802
858
  }
803
859
 
804
860
  // Validate key against server before storing
805
- const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://contentgrade.ai';
861
+ const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://content-grade.github.io';
806
862
  blank();
807
863
  process.stdout.write(` ${D}Validating key...${R}`);
808
864
 
@@ -819,9 +875,9 @@ async function cmdActivate() {
819
875
  } else {
820
876
  process.stdout.write('\n');
821
877
  blank();
822
- fail(data.message || 'Invalid or expired license key. Visit contentgrade.ai to check your subscription.');
878
+ fail(data.message || 'Invalid or expired license key. Visit content-grade.github.io/Content-Grade to check your subscription.');
823
879
  blank();
824
- process.exit(2);
880
+ process.exit(1);
825
881
  }
826
882
  } catch {
827
883
  // Server unreachable — allow offline activation with a warning
@@ -873,7 +929,7 @@ async function cmdBatch(dirPath) {
873
929
  console.log(` Usage: ${CY}content-grade batch <directory>${R}`);
874
930
  console.log(` Example: ${CY}content-grade batch ./posts${R}`);
875
931
  blank();
876
- process.exit(2);
932
+ process.exit(1);
877
933
  }
878
934
 
879
935
  const safeDirPath = sanitizeFilePath(dirPath);
@@ -881,7 +937,7 @@ async function cmdBatch(dirPath) {
881
937
  blank();
882
938
  fail(`Invalid directory path.`);
883
939
  blank();
884
- process.exit(2);
940
+ process.exit(1);
885
941
  }
886
942
  dirPath = safeDirPath;
887
943
 
@@ -890,7 +946,7 @@ async function cmdBatch(dirPath) {
890
946
  blank();
891
947
  fail(`Directory not found: ${absDir}`);
892
948
  blank();
893
- process.exit(2);
949
+ process.exit(1);
894
950
  }
895
951
 
896
952
  const st = statSync(absDir);
@@ -898,7 +954,7 @@ async function cmdBatch(dirPath) {
898
954
  blank();
899
955
  fail(`${dirPath} is not a directory. Use ${CY}content-grade analyze${R} for single files.`);
900
956
  blank();
901
- process.exit(2);
957
+ process.exit(1);
902
958
  }
903
959
 
904
960
  const files = [];
@@ -1246,10 +1302,11 @@ function cmdHelp() {
1246
1302
  blank();
1247
1303
  console.log(` ${B}QUICK START${R}`);
1248
1304
  blank();
1249
- console.log(` ${CY}npx content-grade headline "Your Headline Here"${R} ${D}# grade a headline (fastest, ~5s)${R}`);
1250
- console.log(` ${CY}npx content-grade analyze ./my-post.md${R} ${D}# full AI content analysis (~20s)${R}`);
1251
- console.log(` ${CY}npx content-grade demo${R} ${D}# live demo on sample content${R}`);
1252
- console.log(` ${CY}npx content-grade start${R} ${D}# launch web dashboard (6 tools)${R}`);
1305
+ console.log(` ${CY}npx content-grade headline "Your Headline Here"${R} ${D}# grade a headline (fastest, ~5s)${R}`);
1306
+ console.log(` ${CY}npx content-grade analyze ./my-post.md${R} ${D}# full AI analysis of a file (~20s)${R}`);
1307
+ console.log(` ${CY}npx content-grade analyze https://example.com${R} ${D}# audit any live URL (~20s)${R}`);
1308
+ console.log(` ${CY}npx content-grade demo${R} ${D}# live demo on sample content${R}`);
1309
+ console.log(` ${CY}npx content-grade start${R} ${D}# launch web dashboard (6 tools)${R}`);
1253
1310
  blank();
1254
1311
 
1255
1312
  console.log(` ${B}USAGE${R}`);
@@ -1270,6 +1327,8 @@ function cmdHelp() {
1270
1327
  blank();
1271
1328
  console.log(` ${CY}batch <directory>${R} ${MG}[Pro]${R} Analyze all .md/.txt files in a directory`);
1272
1329
  blank();
1330
+ console.log(` ${CY}seo-audit <url>${R} Fetch a live URL and analyze its content (SEO, readability, structure)`);
1331
+ blank();
1273
1332
  console.log(` ${CY}headline "<text>"${R} Grade a single headline (4 copywriting frameworks)`);
1274
1333
  blank();
1275
1334
  console.log(` ${CY}activate${R} Enter license key to unlock Pro features`);
@@ -1291,6 +1350,9 @@ function cmdHelp() {
1291
1350
  blank();
1292
1351
  console.log(` ${CY}--json${R} Output raw JSON (great for CI pipelines)`);
1293
1352
  console.log(` ${CY}--quiet${R} Output score number only (for scripting)`);
1353
+ console.log(` ${CY}--save [file]${R} Save analysis to a markdown file (default: <input>.content-grade.md)`);
1354
+ console.log(` ${CY}--ci${R} Exit 0 if score passes threshold, exit 1 if it fails`);
1355
+ console.log(` ${CY}--threshold <n>${R} Score threshold for --ci mode (default: 60)`);
1294
1356
  console.log(` ${CY}--verbose${R} Show debug info: model, timing, raw response length`);
1295
1357
  console.log(` ${CY}--version${R} Print version and exit`);
1296
1358
  console.log(` ${CY}--no-telemetry${R} Skip usage tracking for this invocation`);
@@ -1304,8 +1366,11 @@ function cmdHelp() {
1304
1366
  console.log(` ${D}# Grade README.md (alias)${R}`);
1305
1367
  console.log(` ${CY}content-grade grade README.md${R}`);
1306
1368
  blank();
1307
- console.log(` ${D}# CI pipeline exit 1 if score < threshold${R}`);
1308
- console.log(` ${CY}content-grade analyze ./blog.md --json | jq '.total_score'${R}`);
1369
+ console.log(` ${D}# Audit a live URL${R}`);
1370
+ console.log(` ${CY}content-grade seo-audit https://yoursite.com/blog-post${R}`);
1371
+ blank();
1372
+ console.log(` ${D}# CI pipeline — fail build if score is below 70${R}`);
1373
+ console.log(` ${CY}content-grade analyze ./blog.md --ci --threshold 70${R}`);
1309
1374
  blank();
1310
1375
  console.log(` ${D}# Get score number only (for scripts)${R}`);
1311
1376
  console.log(` ${CY}content-grade analyze ./post.md --quiet${R}`);
@@ -1426,18 +1491,24 @@ function cmdQuickDemo() {
1426
1491
  hr();
1427
1492
  console.log(` ${D}↑ This is a ${B}sample${R}${D} — your real content gets a live AI analysis in ~20 seconds.${R}`);
1428
1493
  blank();
1429
- console.log(` ${B}${CY}Try it now:${R}`);
1430
- blank();
1431
- console.log(` ${CY}npx content-grade headline "Your headline here"${R} ${D}# fastest — grade any headline${R}`);
1432
- console.log(` ${CY}npx content-grade analyze ./your-post.md${R} ${D}# full AI analysis of a file${R}`);
1433
- console.log(` ${CY}npx content-grade demo${R} ${D}# live demo — takes ~20s with Claude${R}`);
1434
- blank();
1435
-
1494
+ hr();
1436
1495
  if (isFirstRun()) {
1496
+ console.log(` ${B}${CY}Your next step:${R}`);
1497
+ blank();
1498
+ console.log(` ${CY}npx content-grade headline "Your headline here"${R}`);
1499
+ console.log(` ${D} Grade any headline in ~5 seconds — no file needed, works right now.${R}`);
1500
+ blank();
1501
+ console.log(` ${D}Then analyze a full post: ${CY}npx content-grade analyze ./your-post.md${R}`);
1502
+ blank();
1437
1503
  console.log(` ${D}Requires Claude CLI (free): ${CY}claude.ai/code${R}${D} → install → ${CY}claude login${R}`);
1438
- console.log(` ${D}Setup check: ${CY}npx content-grade init${R}`);
1504
+ console.log(` ${D}Setup check: ${CY}npx content-grade init${R}`);
1505
+ } else {
1506
+ console.log(` ${B}${CY}Grade a headline right now:${R}`);
1439
1507
  blank();
1508
+ console.log(` ${CY}npx content-grade headline "Your headline here"${R} ${D}# ~5 seconds${R}`);
1509
+ console.log(` ${CY}npx content-grade analyze ./your-post.md${R} ${D}# full AI analysis${R}`);
1440
1510
  }
1511
+ blank();
1441
1512
  }
1442
1513
 
1443
1514
  // ── Demo command ──────────────────────────────────────────────────────────────
@@ -1555,10 +1626,10 @@ async function cmdDemo() {
1555
1626
 
1556
1627
  // ── URL fetcher ───────────────────────────────────────────────────────────────
1557
1628
 
1558
- function fetchUrl(url) {
1629
+ function fetchUrl(url, { rejectUnauthorized = true } = {}) {
1559
1630
  return new Promise((resolve, reject) => {
1560
1631
  const get = url.startsWith('https') ? httpsGet : httpGet;
1561
- const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/content-grade/Content-Grade)' }, timeout: 15000 }, (res) => {
1632
+ const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/content-grade/Content-Grade)' }, timeout: 15000, rejectUnauthorized }, (res) => {
1562
1633
  // Follow one redirect — validate location is http/https before following
1563
1634
  if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
1564
1635
  const loc = res.headers.location;
@@ -1567,7 +1638,7 @@ function fetchUrl(url) {
1567
1638
  res.resume();
1568
1639
  return;
1569
1640
  }
1570
- fetchUrl(loc).then(resolve).catch(reject);
1641
+ fetchUrl(loc, { rejectUnauthorized }).then(resolve).catch(reject);
1571
1642
  res.resume();
1572
1643
  return;
1573
1644
  }
@@ -1581,7 +1652,16 @@ function fetchUrl(url) {
1581
1652
  res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8', 0, 500000)));
1582
1653
  res.on('error', reject);
1583
1654
  });
1584
- req.on('error', reject);
1655
+ req.on('error', (err) => {
1656
+ // TLS cert failures: retry without verification (dev tool — user may be auditing
1657
+ // internal sites with self-signed certs or proxies that intercept TLS)
1658
+ const isCertErr = err.code && /CERT|SELF_SIGNED|UNABLE_TO/i.test(err.code);
1659
+ if (isCertErr && rejectUnauthorized) {
1660
+ fetchUrl(url, { rejectUnauthorized: false }).then(resolve).catch(reject);
1661
+ return;
1662
+ }
1663
+ reject(err);
1664
+ });
1585
1665
  req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
1586
1666
  });
1587
1667
  }
@@ -1611,13 +1691,13 @@ async function cmdAnalyzeUrl(url) {
1611
1691
  blank();
1612
1692
  fail(`Invalid URL: must start with http:// or https://`);
1613
1693
  blank();
1614
- process.exit(2);
1694
+ process.exit(1);
1615
1695
  }
1616
1696
  if (sanitizedUrl.length > 2048) {
1617
1697
  blank();
1618
1698
  fail(`URL too long (${sanitizedUrl.length} chars). Maximum is 2048.`);
1619
1699
  blank();
1620
- process.exit(2);
1700
+ process.exit(1);
1621
1701
  }
1622
1702
  url = sanitizedUrl;
1623
1703
 
@@ -1716,13 +1796,101 @@ function findBestContentFile(dirPath) {
1716
1796
  return scored[0].path;
1717
1797
  }
1718
1798
 
1799
+ // ── Save analysis to markdown ─────────────────────────────────────────────────
1800
+
1801
+ function analysisToMarkdown(result, sourceName, analyzedAt) {
1802
+ const grade = result.grade ?? gradeLetter(result.total_score).replace(/\x1b\[[0-9;]*m/g, '');
1803
+ const lines = [];
1804
+ lines.push(`# ContentGrade Report: ${sourceName}`);
1805
+ lines.push(``);
1806
+ lines.push(`Analyzed: ${analyzedAt} `);
1807
+ lines.push(`Overall score: **${result.total_score}/100 (${grade})** `);
1808
+ if (result.content_type) lines.push(`Content type: ${result.content_type.replace('_', ' ')}`);
1809
+ lines.push(``);
1810
+
1811
+ if (result.headline?.text) {
1812
+ lines.push(`## Headline`);
1813
+ lines.push(``);
1814
+ lines.push(`> ${result.headline.text}`);
1815
+ lines.push(``);
1816
+ lines.push(`Score: ${result.headline.score}/100`);
1817
+ if (result.headline.feedback) lines.push(`${result.headline.feedback}`);
1818
+ lines.push(``);
1819
+ }
1820
+
1821
+ if (result.one_line_verdict) {
1822
+ lines.push(`## Verdict`);
1823
+ lines.push(``);
1824
+ lines.push(`${result.one_line_verdict}`);
1825
+ lines.push(``);
1826
+ }
1827
+
1828
+ if (result.dimensions) {
1829
+ lines.push(`## Dimension Breakdown`);
1830
+ lines.push(``);
1831
+ for (const [, dim] of Object.entries(result.dimensions)) {
1832
+ lines.push(`**${dim.label}**: ${dim.score}/100`);
1833
+ if (dim.feedback) lines.push(`${dim.feedback}`);
1834
+ lines.push(``);
1835
+ }
1836
+ }
1837
+
1838
+ if (result.strengths?.length) {
1839
+ lines.push(`## Strengths`);
1840
+ lines.push(``);
1841
+ for (const s of result.strengths) lines.push(`- ${s}`);
1842
+ lines.push(``);
1843
+ }
1844
+
1845
+ if (result.improvements?.length) {
1846
+ lines.push(`## Top Improvements`);
1847
+ lines.push(``);
1848
+ for (const imp of result.improvements) {
1849
+ const pri = imp.priority === 'high' ? '🔴' : '🟡';
1850
+ lines.push(`### ${pri} ${imp.issue}`);
1851
+ lines.push(``);
1852
+ lines.push(`**Fix:** ${imp.fix}`);
1853
+ lines.push(``);
1854
+ }
1855
+ }
1856
+
1857
+ if (result.headline_rewrites?.length) {
1858
+ lines.push(`## Headline Rewrites`);
1859
+ lines.push(``);
1860
+ for (let i = 0; i < result.headline_rewrites.length; i++) {
1861
+ lines.push(`${i + 1}. ${result.headline_rewrites[i]}`);
1862
+ }
1863
+ lines.push(``);
1864
+ }
1865
+
1866
+ lines.push(`---`);
1867
+ lines.push(`Generated by [ContentGrade](https://content-grade.github.io/Content-Grade/) v${_version}`);
1868
+ return lines.join('\n');
1869
+ }
1870
+
1719
1871
  // ── Router ────────────────────────────────────────────────────────────────────
1720
1872
 
1721
1873
  const _rawArgs = process.argv.slice(2);
1722
1874
  const _jsonMode = _rawArgs.includes('--json');
1723
1875
  const _quietMode = _rawArgs.includes('--quiet');
1724
1876
  const _verboseMode = _rawArgs.includes('--verbose') || _rawArgs.includes('-v') && !_rawArgs.includes('--version');
1725
- const args = _rawArgs.filter(a => !['--no-telemetry', '--json', '--quiet', '--verbose'].includes(a));
1877
+ const _ciMode = _rawArgs.includes('--ci');
1878
+ const _threshIdx = _rawArgs.indexOf('--threshold');
1879
+ const _ciThreshold = (_threshIdx !== -1 && _rawArgs[_threshIdx + 1])
1880
+ ? (parseInt(_rawArgs[_threshIdx + 1], 10) || 60)
1881
+ : 60;
1882
+ const _saveIdx = _rawArgs.indexOf('--save');
1883
+ const _saveMode = _saveIdx !== -1;
1884
+ const _savePath = (_saveMode && _rawArgs[_saveIdx + 1] && !_rawArgs[_saveIdx + 1].startsWith('-'))
1885
+ ? _rawArgs[_saveIdx + 1]
1886
+ : null;
1887
+ const args = _rawArgs.filter((a, i) => {
1888
+ if (['--no-telemetry', '--json', '--quiet', '--verbose', '--ci', '--save'].includes(a)) return false;
1889
+ if (a === '--threshold') return false;
1890
+ if (i > 0 && _rawArgs[i - 1] === '--threshold') return false;
1891
+ if (_saveMode && i > 0 && _rawArgs[i - 1] === '--save' && !a.startsWith('-')) return false;
1892
+ return true;
1893
+ });
1726
1894
  const raw = args[0];
1727
1895
  const cmd = raw?.toLowerCase();
1728
1896
 
@@ -1739,6 +1907,108 @@ function looksLikePath(s) {
1739
1907
  try { statSync(resolve(process.cwd(), s)); return true; } catch { return false; }
1740
1908
  }
1741
1909
 
1910
+ // Per-command --help: "content-grade <cmd> --help" → command-specific help
1911
+ if (_rawArgs.includes('--help') || _rawArgs.includes('-h')) {
1912
+ switch (cmd) {
1913
+ case 'analyze':
1914
+ case 'analyse':
1915
+ case 'check':
1916
+ case 'grade':
1917
+ blank();
1918
+ console.log(` ${B}content-grade analyze <file>${R} ${D}(aliases: grade, check)${R}`);
1919
+ blank();
1920
+ console.log(` Analyzes written content and returns an AI-powered score (0–100) with`);
1921
+ console.log(` dimension breakdown, specific improvements, and headline rewrites.`);
1922
+ blank();
1923
+ console.log(` ${B}Arguments:${R}`);
1924
+ console.log(` ${CY}<file>${R} Path to a .md, .txt, or .mdx file`);
1925
+ blank();
1926
+ console.log(` ${B}Flags:${R}`);
1927
+ console.log(` ${CY}--json${R} Output raw JSON (great for CI pipelines)`);
1928
+ console.log(` ${CY}--quiet${R} Output score number only (for scripting)`);
1929
+ console.log(` ${CY}--ci${R} Exit 0 if score passes, 1 if it fails`);
1930
+ console.log(` ${CY}--threshold <n>${R} Score threshold for --ci (default: 60)`);
1931
+ console.log(` ${CY}--verbose${R} Show debug info: model, timing, response length`);
1932
+ blank();
1933
+ console.log(` ${B}Examples:${R}`);
1934
+ console.log(` ${CY}content-grade analyze ./blog-post.md${R}`);
1935
+ console.log(` ${CY}content-grade analyze ./readme.md --json${R}`);
1936
+ console.log(` ${CY}content-grade analyze ./copy.md --ci --threshold 70${R}`);
1937
+ console.log(` ${D}# store score in a variable${R}`);
1938
+ console.log(` ${CY}score=\$(content-grade analyze ./post.md --quiet)${R}`);
1939
+ blank();
1940
+ process.exit(0);
1941
+ break;
1942
+ case 'headline':
1943
+ blank();
1944
+ console.log(` ${B}content-grade headline "<text>"${R}`);
1945
+ blank();
1946
+ console.log(` Grades a single headline using 4 direct response copywriting frameworks:`);
1947
+ console.log(` Rule of One, Value Equation, Readability, and Proof/Promise.`);
1948
+ console.log(` Returns a 0–100 score, verdict, and 2 stronger rewrites. Takes ~5 seconds.`);
1949
+ blank();
1950
+ console.log(` ${B}Arguments:${R}`);
1951
+ console.log(` ${CY}"<text>"${R} The headline to grade (quote it to avoid shell expansion)`);
1952
+ blank();
1953
+ console.log(` ${B}Flags:${R}`);
1954
+ console.log(` ${CY}--json${R} Output raw JSON`);
1955
+ console.log(` ${CY}--quiet${R} Output score number only`);
1956
+ console.log(` ${CY}--verbose${R} Show debug info`);
1957
+ blank();
1958
+ console.log(` ${B}Examples:${R}`);
1959
+ console.log(` ${CY}content-grade headline "The 5-Minute Fix That Doubles Landing Page Conversions"${R}`);
1960
+ console.log(` ${CY}content-grade headline "How We Grew From 0 to 10k Users" --json${R}`);
1961
+ blank();
1962
+ process.exit(0);
1963
+ break;
1964
+ case 'seo-audit':
1965
+ case 'seoaudit':
1966
+ blank();
1967
+ console.log(` ${B}content-grade seo-audit <url>${R}`);
1968
+ blank();
1969
+ console.log(` Fetches a live URL, extracts readable content, and runs the same full AI`);
1970
+ console.log(` analysis as the analyze command. Good for auditing competitors or your own pages.`);
1971
+ blank();
1972
+ console.log(` ${B}Arguments:${R}`);
1973
+ console.log(` ${CY}<url>${R} Full URL starting with http:// or https://`);
1974
+ blank();
1975
+ console.log(` ${B}Flags:${R}`);
1976
+ console.log(` ${CY}--json${R} Output raw JSON`);
1977
+ console.log(` ${CY}--quiet${R} Output score number only`);
1978
+ console.log(` ${CY}--ci${R} Exit 0/1 based on score threshold`);
1979
+ console.log(` ${CY}--threshold <n>${R} Score threshold for --ci (default: 60)`);
1980
+ blank();
1981
+ console.log(` ${B}Examples:${R}`);
1982
+ console.log(` ${CY}content-grade seo-audit https://yoursite.com/blog-post${R}`);
1983
+ console.log(` ${CY}content-grade seo-audit https://example.com --json${R}`);
1984
+ blank();
1985
+ process.exit(0);
1986
+ break;
1987
+ case 'batch':
1988
+ blank();
1989
+ console.log(` ${B}content-grade batch <directory>${R} ${MG}[Pro]${R}`);
1990
+ blank();
1991
+ console.log(` Analyzes all .md, .txt, and .mdx files in a directory and prints a sorted`);
1992
+ console.log(` summary with scores. Requires a Pro license.`);
1993
+ blank();
1994
+ console.log(` ${B}Arguments:${R}`);
1995
+ console.log(` ${CY}<directory>${R} Path to a directory containing content files`);
1996
+ blank();
1997
+ console.log(` ${B}Flags:${R}`);
1998
+ console.log(` ${CY}--json${R} Output results as a JSON array (replaces styled output)`);
1999
+ blank();
2000
+ console.log(` ${B}Examples:${R}`);
2001
+ console.log(` ${CY}content-grade batch ./posts${R}`);
2002
+ console.log(` ${CY}content-grade batch ./content --json${R}`);
2003
+ blank();
2004
+ process.exit(0);
2005
+ break;
2006
+ default:
2007
+ cmdHelp();
2008
+ process.exit(0);
2009
+ }
2010
+ }
2011
+
1742
2012
  switch (cmd) {
1743
2013
  case 'analyze':
1744
2014
  case 'analyse':
@@ -1801,6 +2071,26 @@ switch (cmd) {
1801
2071
  });
1802
2072
  break;
1803
2073
 
2074
+ case 'seo-audit':
2075
+ case 'seoaudit':
2076
+ recordEvent({ event: 'command', command: 'seo-audit' });
2077
+ if (!args[1]) {
2078
+ blank();
2079
+ fail(`No URL specified.`);
2080
+ blank();
2081
+ console.log(` ${B}Usage:${R} ${CY}content-grade seo-audit <url>${R}`);
2082
+ console.log(` ${B}Example:${R} ${CY}content-grade seo-audit https://yoursite.com/blog-post${R}`);
2083
+ blank();
2084
+ process.exit(1);
2085
+ }
2086
+ cmdAnalyzeUrl(args[1]).catch(err => {
2087
+ blank();
2088
+ fail(`Unexpected error: ${err.message}`);
2089
+ blank();
2090
+ process.exit(1);
2091
+ });
2092
+ break;
2093
+
1804
2094
  case 'batch':
1805
2095
  recordEvent({ event: 'command', command: 'batch' });
1806
2096
  cmdBatch(args[1]).catch(err => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-grade",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
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": {