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 +30 -181
- package/README.md +24 -12
- package/bin/content-grade.js +346 -56
- package/package.json +1 -1
package/CONTRIBUTING.md
CHANGED
|
@@ -1,198 +1,47 @@
|
|
|
1
|
-
# Contributing to
|
|
1
|
+
# Contributing to Content-Grade
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
11
|
+
## Suggesting features
|
|
8
12
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
## Code style
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
Scripts from `package.json` tell you what tools are in play:
|
|
32
31
|
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
+
[](https://github.com/StanislavBG/Content-Grade/actions/workflows/ci.yml)
|
|
5
6
|
[](https://www.npmjs.com/package/content-grade)
|
|
6
7
|
[](https://www.npmjs.com/package/content-grade)
|
|
7
8
|
[](https://www.npmjs.com/package/content-grade)
|
|
8
9
|
[](https://opensource.org/licenses/MIT)
|
|
9
10
|
[](https://nodejs.org)
|
|
10
11
|
[](https://claude.ai/code)
|
|
11
|
-
[](https://github.com/
|
|
12
|
+
[](https://github.com/StanislavBG/Content-Grade/discussions)
|
|
12
13
|
[](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 ($
|
|
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/
|
|
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 | $
|
|
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/
|
|
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/
|
|
953
|
-
| [Show & Tell](https://github.com/
|
|
954
|
-
| [Ideas](https://github.com/
|
|
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** ($
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
package/bin/content-grade.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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) {
|
|
392
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|
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(
|
|
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://
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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}
|
|
1250
|
-
console.log(` ${CY}npx content-grade analyze ./my-post.md${R}
|
|
1251
|
-
console.log(` ${CY}npx content-grade
|
|
1252
|
-
console.log(` ${CY}npx content-grade
|
|
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}#
|
|
1308
|
-
console.log(` ${CY}content-grade
|
|
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
|
-
|
|
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:
|
|
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',
|
|
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(
|
|
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(
|
|
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
|
|
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