content-grade 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +30 -181
- package/README.md +4 -1
- package/bin/content-grade.js +318 -66
- 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
|
@@ -63,7 +63,7 @@ npx content-grade 'blog/**/*.md'
|
|
|
63
63
|
|
|
64
64
|
**JSON output for CI integration:**
|
|
65
65
|
```bash
|
|
66
|
-
npx content-grade README.md --
|
|
66
|
+
npx content-grade README.md --json
|
|
67
67
|
# Returns structured JSON with score, grade, dimensions, and improvements
|
|
68
68
|
# Exit code 1 if score < 50 — useful for blocking low-quality merges
|
|
69
69
|
```
|
|
@@ -549,6 +549,7 @@ tests/
|
|
|
549
549
|
|
|
550
550
|
**Documentation:**
|
|
551
551
|
- [`docs/getting-started.md`](./docs/getting-started.md) — step-by-step first-run guide
|
|
552
|
+
- [`docs/cli-reference.md`](./docs/cli-reference.md) — full CLI reference: all commands, flags, output schema, exit codes
|
|
552
553
|
- [`docs/examples.md`](./docs/examples.md) — real-world examples for all 6 tools, CI/CD workflows, Node.js integration
|
|
553
554
|
- [`docs/api.md`](./docs/api.md) — full REST API reference
|
|
554
555
|
|
|
@@ -974,6 +975,8 @@ Power users who go beyond early adoption get early access to `@beta` releases, a
|
|
|
974
975
|
|
|
975
976
|
See **[CONTRIBUTING.md](CONTRIBUTING.md)** for local dev setup, PR guidelines, and how to write good bug reports. The short version: clone → `pnpm install` → `pnpm dev` → file a PR. All contributions get a review within 48 hours.
|
|
976
977
|
|
|
978
|
+
Looking for a starting point? Browse **[`good first issue`](https://github.com/StanislavBG/Content-Grade/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)** — scoped tasks completable in a few hours with no prior codebase knowledge required. Each one has acceptance criteria and a pointer to exactly which file to touch.
|
|
979
|
+
|
|
977
980
|
### Roadmap
|
|
978
981
|
|
|
979
982
|
**[ROADMAP.md](ROADMAP.md)** — what's built, what's next, what's planned, and what we're deliberately not doing. Open a [Discussion](https://github.com/StanislavBG/Content-Grade/discussions/new?category=ideas) to challenge or extend it.
|
package/bin/content-grade.js
CHANGED
|
@@ -66,7 +66,7 @@ function incrementUsage() {
|
|
|
66
66
|
return u.count;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const FREE_DAILY_LIMIT =
|
|
69
|
+
const FREE_DAILY_LIMIT = 5;
|
|
70
70
|
|
|
71
71
|
function checkDailyLimit() {
|
|
72
72
|
if (isProUser()) return { ok: true };
|
|
@@ -252,20 +252,30 @@ SCORING CALIBRATION:
|
|
|
252
252
|
|
|
253
253
|
async function cmdAnalyze(filePath) {
|
|
254
254
|
if (!filePath) {
|
|
255
|
+
if (_demoMode) {
|
|
256
|
+
return cmdDemo();
|
|
257
|
+
}
|
|
255
258
|
blank();
|
|
256
259
|
fail(`No file specified.`);
|
|
257
260
|
blank();
|
|
258
|
-
console.log(` Usage
|
|
261
|
+
console.log(` ${B}Usage:${R}`);
|
|
259
262
|
console.log(` ${CY}content-grade analyze <file>${R} ${D}(or: check <file>)${R}`);
|
|
260
263
|
blank();
|
|
261
|
-
console.log(` Examples
|
|
262
|
-
console.log(` ${
|
|
263
|
-
console.log(` ${
|
|
264
|
-
console.log(` ${
|
|
264
|
+
console.log(` ${B}Examples:${R}`);
|
|
265
|
+
console.log(` ${CY}content-grade analyze ./blog-post.md${R}`);
|
|
266
|
+
console.log(` ${CY}content-grade check ./email-draft.txt${R}`);
|
|
267
|
+
console.log(` ${CY}content-grade analyze ~/landing-page-copy.md${R}`);
|
|
265
268
|
blank();
|
|
266
|
-
console.log(` ${
|
|
269
|
+
console.log(` ${GN}Tip:${R} See it in action first — no file needed:`);
|
|
270
|
+
console.log(` ${CY}content-grade --demo${R} ${D}# live AI analysis on built-in sample${R}`);
|
|
271
|
+
console.log(` ${CY}content-grade demo${R} ${D}# same thing${R}`);
|
|
267
272
|
blank();
|
|
268
|
-
process.exit(
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// If input looks like a URL, delegate to URL analyzer
|
|
277
|
+
if (/^https?:\/\//i.test(filePath)) {
|
|
278
|
+
return cmdAnalyzeUrl(filePath);
|
|
269
279
|
}
|
|
270
280
|
|
|
271
281
|
const safeFilePath = sanitizeFilePath(filePath);
|
|
@@ -273,7 +283,7 @@ async function cmdAnalyze(filePath) {
|
|
|
273
283
|
blank();
|
|
274
284
|
fail(`Invalid file path.`);
|
|
275
285
|
blank();
|
|
276
|
-
process.exit(
|
|
286
|
+
process.exit(1);
|
|
277
287
|
}
|
|
278
288
|
filePath = safeFilePath;
|
|
279
289
|
|
|
@@ -285,7 +295,7 @@ async function cmdAnalyze(filePath) {
|
|
|
285
295
|
console.log(` ${YL}Check the path and try again.${R}`);
|
|
286
296
|
console.log(` ${D}Tip: use a relative path like ./my-file.md or an absolute path.${R}`);
|
|
287
297
|
blank();
|
|
288
|
-
process.exit(
|
|
298
|
+
process.exit(1);
|
|
289
299
|
}
|
|
290
300
|
|
|
291
301
|
// Guard: reject directories
|
|
@@ -296,7 +306,7 @@ async function cmdAnalyze(filePath) {
|
|
|
296
306
|
blank();
|
|
297
307
|
console.log(` ${D}Pass a text file (.md, .txt) — not a directory.${R}`);
|
|
298
308
|
blank();
|
|
299
|
-
process.exit(
|
|
309
|
+
process.exit(1);
|
|
300
310
|
}
|
|
301
311
|
|
|
302
312
|
// Guard: reject files over 500 KB before reading into memory
|
|
@@ -307,7 +317,7 @@ async function cmdAnalyze(filePath) {
|
|
|
307
317
|
blank();
|
|
308
318
|
console.log(` ${YL}Tip:${R} Copy the relevant section into a new file and analyze that.`);
|
|
309
319
|
blank();
|
|
310
|
-
process.exit(
|
|
320
|
+
process.exit(1);
|
|
311
321
|
}
|
|
312
322
|
|
|
313
323
|
const content = readFileSync(absPath, 'utf8');
|
|
@@ -320,14 +330,14 @@ async function cmdAnalyze(filePath) {
|
|
|
320
330
|
console.log(` ${YL}ContentGrade analyzes written content — blog posts, emails, ad copy, landing pages.${R}`);
|
|
321
331
|
console.log(` ${D}Supported formats: .md, .txt, .mdx, or any plain-text file.${R}`);
|
|
322
332
|
blank();
|
|
323
|
-
process.exit(
|
|
333
|
+
process.exit(1);
|
|
324
334
|
}
|
|
325
335
|
|
|
326
336
|
if (content.trim().length < 20) {
|
|
327
337
|
blank();
|
|
328
338
|
fail(`File is too short to analyze (${content.trim().length} chars). Add some content and try again.`);
|
|
329
339
|
blank();
|
|
330
|
-
process.exit(
|
|
340
|
+
process.exit(1);
|
|
331
341
|
}
|
|
332
342
|
|
|
333
343
|
// Free tier daily limit
|
|
@@ -338,9 +348,9 @@ async function cmdAnalyze(filePath) {
|
|
|
338
348
|
blank();
|
|
339
349
|
console.log(` ${B}Options:${R}`);
|
|
340
350
|
console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
|
|
341
|
-
console.log(` ${D}· Unlock
|
|
351
|
+
console.log(` ${D}· Unlock unlimited: ${CY}content-grade activate${R}`);
|
|
342
352
|
blank();
|
|
343
|
-
console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://
|
|
353
|
+
console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://buy.stripe.com/5kQeVfew48dT7nf2W48k801${R}`);
|
|
344
354
|
blank();
|
|
345
355
|
process.exit(1);
|
|
346
356
|
}
|
|
@@ -491,20 +501,38 @@ async function cmdAnalyze(filePath) {
|
|
|
491
501
|
// Track usage
|
|
492
502
|
incrementUsage();
|
|
493
503
|
|
|
494
|
-
//
|
|
504
|
+
// Save to file if --save flag is set
|
|
505
|
+
if (_saveMode && result) {
|
|
506
|
+
const defaultSaveName = basename(absPath, extname(absPath)) + '.content-grade.md';
|
|
507
|
+
const outPath = resolve(process.cwd(), _savePath || defaultSaveName);
|
|
508
|
+
try {
|
|
509
|
+
writeFileSync(outPath, analysisToMarkdown(result, basename(absPath), new Date().toISOString().slice(0, 10)), 'utf8');
|
|
510
|
+
blank();
|
|
511
|
+
ok(`Saved to ${outPath}`);
|
|
512
|
+
} catch (saveErr) {
|
|
513
|
+
warn(`Could not save: ${saveErr.message}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Next step — graduated CTA after user has seen value
|
|
495
518
|
blank();
|
|
519
|
+
hr();
|
|
496
520
|
if (isProUser()) {
|
|
497
|
-
console.log(` ${D}Pro active · ${CY}content-grade
|
|
521
|
+
console.log(` ${D}Pro active · Next: ${CY}content-grade batch ./posts/${R}${D} to analyze a whole directory${R}`);
|
|
498
522
|
} else {
|
|
499
523
|
const usage = getUsage();
|
|
500
524
|
const remaining = Math.max(0, FREE_DAILY_LIMIT - usage.count);
|
|
501
|
-
|
|
502
|
-
console.log(` ${MG}${B}Unlock batch mode:${R} ${CY}content-grade activate${R}`);
|
|
503
|
-
console.log(` ${D} · Analyze entire directories in one command${R}`);
|
|
504
|
-
console.log(` ${D} · 100 checks/day (${remaining} remaining today on free tier)${R}`);
|
|
505
|
-
console.log(` ${D} · Get a license at ${CY}content-grade.github.io/Content-Grade${R}`);
|
|
525
|
+
console.log(` ${B}What to do next:${R}`);
|
|
506
526
|
blank();
|
|
507
|
-
console.log(` ${
|
|
527
|
+
console.log(` ${CY}content-grade headline "Your title here"${R} ${D}# grade a headline in ~5s${R}`);
|
|
528
|
+
console.log(` ${CY}content-grade analyze ./another-post.md${R} ${D}# analyze another file${R}`);
|
|
529
|
+
console.log(` ${CY}content-grade analyze https://yoursite.com/post${R} ${D}# audit any live URL${R}`);
|
|
530
|
+
blank();
|
|
531
|
+
if (remaining <= 10) {
|
|
532
|
+
console.log(` ${YL}${remaining} free checks left today.${R} Unlock unlimited: ${CY}content-grade activate${R}`);
|
|
533
|
+
} else {
|
|
534
|
+
console.log(` ${D}${remaining} free checks left today · Pro ($9/mo): ${CY}content-grade.github.io/Content-Grade${R}`);
|
|
535
|
+
}
|
|
508
536
|
}
|
|
509
537
|
|
|
510
538
|
// CI exit code — shown after full output so user sees the score before exit
|
|
@@ -563,7 +591,7 @@ async function cmdHeadline(text) {
|
|
|
563
591
|
console.log(` ${D}content-grade headline "How We Grew From 0 to 10k Users Without Ads"${R}`);
|
|
564
592
|
console.log(` ${D}content-grade headline "Stop Doing This One Thing in Your Email Subject Lines"${R}`);
|
|
565
593
|
blank();
|
|
566
|
-
process.exit(
|
|
594
|
+
process.exit(1);
|
|
567
595
|
}
|
|
568
596
|
|
|
569
597
|
// Guard: reject oversized input
|
|
@@ -574,7 +602,7 @@ async function cmdHeadline(text) {
|
|
|
574
602
|
blank();
|
|
575
603
|
console.log(` ${YL}Tip:${R} Trim your headline to the core message and try again.`);
|
|
576
604
|
blank();
|
|
577
|
-
process.exit(
|
|
605
|
+
process.exit(1);
|
|
578
606
|
}
|
|
579
607
|
|
|
580
608
|
// Free tier daily limit
|
|
@@ -585,31 +613,41 @@ async function cmdHeadline(text) {
|
|
|
585
613
|
blank();
|
|
586
614
|
console.log(` ${B}Options:${R}`);
|
|
587
615
|
console.log(` ${D}· Wait until tomorrow (limit resets at midnight)${R}`);
|
|
588
|
-
console.log(` ${D}· Unlock
|
|
616
|
+
console.log(` ${D}· Unlock unlimited: ${CY}content-grade activate${R}`);
|
|
589
617
|
blank();
|
|
590
|
-
console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://
|
|
618
|
+
console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://buy.stripe.com/5kQeVfew48dT7nf2W48k801${R}`);
|
|
591
619
|
blank();
|
|
592
620
|
process.exit(1);
|
|
593
621
|
}
|
|
594
622
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
623
|
+
if (!_jsonMode && !_quietMode) {
|
|
624
|
+
banner();
|
|
625
|
+
console.log(` ${B}Grading headline:${R}`);
|
|
626
|
+
console.log(` ${D}"${text}"${R}`);
|
|
627
|
+
blank();
|
|
628
|
+
}
|
|
599
629
|
|
|
600
630
|
if (!checkClaude()) {
|
|
601
631
|
fail(`Claude CLI not found. Run: ${CY}content-grade init${R}`);
|
|
602
632
|
process.exit(1);
|
|
603
633
|
}
|
|
604
634
|
|
|
605
|
-
process.stdout.write(` ${D}Analyzing...${R}`);
|
|
635
|
+
if (!_jsonMode && !_quietMode) process.stdout.write(` ${D}Analyzing...${R}`);
|
|
606
636
|
|
|
607
637
|
let result;
|
|
608
638
|
try {
|
|
609
639
|
const raw = await askClaude(`Grade this headline: "${text}"`, HEADLINE_SYSTEM, 'claude-haiku-4-5-20251001');
|
|
610
|
-
process.stdout.write(`\r ${GN}✓${R} Done${' '.repeat(20)}\n`);
|
|
640
|
+
if (!_jsonMode && !_quietMode) process.stdout.write(`\r ${GN}✓${R} Done${' '.repeat(20)}\n`);
|
|
611
641
|
result = parseJSON(raw);
|
|
612
642
|
recordEvent({ event: 'headline_result', score: result.score });
|
|
643
|
+
if (_jsonMode) {
|
|
644
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (_quietMode) {
|
|
648
|
+
process.stdout.write(`${result.score}\n`);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
613
651
|
} catch (err) {
|
|
614
652
|
process.stdout.write(`\n`);
|
|
615
653
|
blank();
|
|
@@ -667,7 +705,7 @@ async function cmdHeadline(text) {
|
|
|
667
705
|
console.log(` ${D}Compare two headlines: ${CY}content-grade start${R} → HeadlineGrader compare${R}`);
|
|
668
706
|
blank();
|
|
669
707
|
if (!isProUser()) {
|
|
670
|
-
console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://
|
|
708
|
+
console.log(` ${MG}Upgrade to Pro ($9/mo) for unlimited analyses →${R} ${CY}https://buy.stripe.com/5kQeVfew48dT7nf2W48k801${R}`);
|
|
671
709
|
blank();
|
|
672
710
|
}
|
|
673
711
|
}
|
|
@@ -821,11 +859,11 @@ async function cmdActivate() {
|
|
|
821
859
|
blank();
|
|
822
860
|
fail(`Invalid key — must be at least 8 characters.`);
|
|
823
861
|
blank();
|
|
824
|
-
process.exit(
|
|
862
|
+
process.exit(1);
|
|
825
863
|
}
|
|
826
864
|
|
|
827
865
|
// Validate key against server before storing
|
|
828
|
-
const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://
|
|
866
|
+
const serverUrl = process.env.CONTENT_GRADE_SERVER_URL || 'https://content-grade.github.io';
|
|
829
867
|
blank();
|
|
830
868
|
process.stdout.write(` ${D}Validating key...${R}`);
|
|
831
869
|
|
|
@@ -842,9 +880,9 @@ async function cmdActivate() {
|
|
|
842
880
|
} else {
|
|
843
881
|
process.stdout.write('\n');
|
|
844
882
|
blank();
|
|
845
|
-
fail(data.message || 'Invalid or expired license key. Visit
|
|
883
|
+
fail(data.message || 'Invalid or expired license key. Visit content-grade.github.io/Content-Grade to check your subscription.');
|
|
846
884
|
blank();
|
|
847
|
-
process.exit(
|
|
885
|
+
process.exit(1);
|
|
848
886
|
}
|
|
849
887
|
} catch {
|
|
850
888
|
// Server unreachable — allow offline activation with a warning
|
|
@@ -896,7 +934,7 @@ async function cmdBatch(dirPath) {
|
|
|
896
934
|
console.log(` Usage: ${CY}content-grade batch <directory>${R}`);
|
|
897
935
|
console.log(` Example: ${CY}content-grade batch ./posts${R}`);
|
|
898
936
|
blank();
|
|
899
|
-
process.exit(
|
|
937
|
+
process.exit(1);
|
|
900
938
|
}
|
|
901
939
|
|
|
902
940
|
const safeDirPath = sanitizeFilePath(dirPath);
|
|
@@ -904,7 +942,7 @@ async function cmdBatch(dirPath) {
|
|
|
904
942
|
blank();
|
|
905
943
|
fail(`Invalid directory path.`);
|
|
906
944
|
blank();
|
|
907
|
-
process.exit(
|
|
945
|
+
process.exit(1);
|
|
908
946
|
}
|
|
909
947
|
dirPath = safeDirPath;
|
|
910
948
|
|
|
@@ -913,7 +951,7 @@ async function cmdBatch(dirPath) {
|
|
|
913
951
|
blank();
|
|
914
952
|
fail(`Directory not found: ${absDir}`);
|
|
915
953
|
blank();
|
|
916
|
-
process.exit(
|
|
954
|
+
process.exit(1);
|
|
917
955
|
}
|
|
918
956
|
|
|
919
957
|
const st = statSync(absDir);
|
|
@@ -921,7 +959,7 @@ async function cmdBatch(dirPath) {
|
|
|
921
959
|
blank();
|
|
922
960
|
fail(`${dirPath} is not a directory. Use ${CY}content-grade analyze${R} for single files.`);
|
|
923
961
|
blank();
|
|
924
|
-
process.exit(
|
|
962
|
+
process.exit(1);
|
|
925
963
|
}
|
|
926
964
|
|
|
927
965
|
const files = [];
|
|
@@ -1090,7 +1128,7 @@ function cmdStart() {
|
|
|
1090
1128
|
info(` EmailForge — ${url}/email-forge`);
|
|
1091
1129
|
info(` AudienceDecoder — ${url}/audience`);
|
|
1092
1130
|
blank();
|
|
1093
|
-
info(`Free tier:
|
|
1131
|
+
info(`Free tier: 5 analyses/day. Upgrade at ${url}`);
|
|
1094
1132
|
info(`Press Ctrl+C to stop`);
|
|
1095
1133
|
blank();
|
|
1096
1134
|
openBrowser(url);
|
|
@@ -1269,10 +1307,11 @@ function cmdHelp() {
|
|
|
1269
1307
|
blank();
|
|
1270
1308
|
console.log(` ${B}QUICK START${R}`);
|
|
1271
1309
|
blank();
|
|
1272
|
-
console.log(` ${CY}npx content-grade
|
|
1273
|
-
console.log(` ${CY}npx content-grade
|
|
1274
|
-
console.log(` ${CY}npx content-grade
|
|
1275
|
-
console.log(` ${CY}npx content-grade
|
|
1310
|
+
console.log(` ${CY}npx content-grade --demo${R} ${D}# live AI demo — no file needed, see it first${R}`);
|
|
1311
|
+
console.log(` ${CY}npx content-grade headline "Your Headline Here"${R} ${D}# grade a headline (fastest, ~5s)${R}`);
|
|
1312
|
+
console.log(` ${CY}npx content-grade analyze ./my-post.md${R} ${D}# full AI analysis of a file (~20s)${R}`);
|
|
1313
|
+
console.log(` ${CY}npx content-grade analyze https://example.com${R} ${D}# audit any live URL (~20s)${R}`);
|
|
1314
|
+
console.log(` ${CY}npx content-grade start${R} ${D}# launch web dashboard (6 tools)${R}`);
|
|
1276
1315
|
blank();
|
|
1277
1316
|
|
|
1278
1317
|
console.log(` ${B}USAGE${R}`);
|
|
@@ -1314,8 +1353,10 @@ function cmdHelp() {
|
|
|
1314
1353
|
blank();
|
|
1315
1354
|
console.log(` ${B}FLAGS${R}`);
|
|
1316
1355
|
blank();
|
|
1356
|
+
console.log(` ${CY}--demo${R} Run the live AI demo on built-in sample content`);
|
|
1317
1357
|
console.log(` ${CY}--json${R} Output raw JSON (great for CI pipelines)`);
|
|
1318
1358
|
console.log(` ${CY}--quiet${R} Output score number only (for scripting)`);
|
|
1359
|
+
console.log(` ${CY}--save [file]${R} Save analysis to a markdown file (default: <input>.content-grade.md)`);
|
|
1319
1360
|
console.log(` ${CY}--ci${R} Exit 0 if score passes threshold, exit 1 if it fails`);
|
|
1320
1361
|
console.log(` ${CY}--threshold <n>${R} Score threshold for --ci mode (default: 60)`);
|
|
1321
1362
|
console.log(` ${CY}--verbose${R} Show debug info: model, timing, raw response length`);
|
|
@@ -1456,18 +1497,29 @@ function cmdQuickDemo() {
|
|
|
1456
1497
|
hr();
|
|
1457
1498
|
console.log(` ${D}↑ This is a ${B}sample${R}${D} — your real content gets a live AI analysis in ~20 seconds.${R}`);
|
|
1458
1499
|
blank();
|
|
1459
|
-
|
|
1460
|
-
blank();
|
|
1461
|
-
console.log(` ${CY}npx content-grade headline "Your headline here"${R} ${D}# fastest — grade any headline${R}`);
|
|
1462
|
-
console.log(` ${CY}npx content-grade analyze ./your-post.md${R} ${D}# full AI analysis of a file${R}`);
|
|
1463
|
-
console.log(` ${CY}npx content-grade demo${R} ${D}# live demo — takes ~20s with Claude${R}`);
|
|
1464
|
-
blank();
|
|
1465
|
-
|
|
1500
|
+
hr();
|
|
1466
1501
|
if (isFirstRun()) {
|
|
1467
|
-
console.log(` ${
|
|
1468
|
-
|
|
1502
|
+
console.log(` ${B}${CY}Try it on real content — 3 ways to start:${R}`);
|
|
1503
|
+
blank();
|
|
1504
|
+
console.log(` ${B}1.${R} Grade a headline ${D}(~5 seconds, works right now)${R}`);
|
|
1505
|
+
console.log(` ${CY}npx content-grade headline "Your headline here"${R}`);
|
|
1506
|
+
blank();
|
|
1507
|
+
console.log(` ${B}2.${R} Run the live AI demo ${D}(full analysis on sample content)${R}`);
|
|
1508
|
+
console.log(` ${CY}npx content-grade --demo${R}`);
|
|
1509
|
+
blank();
|
|
1510
|
+
console.log(` ${B}3.${R} Analyze your own file`);
|
|
1511
|
+
console.log(` ${CY}npx content-grade analyze ./your-post.md${R}`);
|
|
1469
1512
|
blank();
|
|
1513
|
+
console.log(` ${D}All three require Claude CLI (free): ${CY}claude.ai/code${R}${D} → install → ${CY}claude login${R}`);
|
|
1514
|
+
console.log(` ${D}First time? Run: ${CY}npx content-grade init${R}${D} to verify setup.${R}`);
|
|
1515
|
+
} else {
|
|
1516
|
+
console.log(` ${B}${CY}Grade your own content:${R}`);
|
|
1517
|
+
blank();
|
|
1518
|
+
console.log(` ${CY}npx content-grade --demo${R} ${D}# live AI demo${R}`);
|
|
1519
|
+
console.log(` ${CY}npx content-grade headline "Your headline here"${R} ${D}# ~5 seconds${R}`);
|
|
1520
|
+
console.log(` ${CY}npx content-grade analyze ./your-post.md${R} ${D}# full analysis${R}`);
|
|
1470
1521
|
}
|
|
1522
|
+
blank();
|
|
1471
1523
|
}
|
|
1472
1524
|
|
|
1473
1525
|
// ── Demo command ──────────────────────────────────────────────────────────────
|
|
@@ -1585,10 +1637,10 @@ async function cmdDemo() {
|
|
|
1585
1637
|
|
|
1586
1638
|
// ── URL fetcher ───────────────────────────────────────────────────────────────
|
|
1587
1639
|
|
|
1588
|
-
function fetchUrl(url) {
|
|
1640
|
+
function fetchUrl(url, { rejectUnauthorized = true } = {}) {
|
|
1589
1641
|
return new Promise((resolve, reject) => {
|
|
1590
1642
|
const get = url.startsWith('https') ? httpsGet : httpGet;
|
|
1591
|
-
const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/content-grade/Content-Grade)' }, timeout: 15000 }, (res) => {
|
|
1643
|
+
const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/content-grade/Content-Grade)' }, timeout: 15000, rejectUnauthorized }, (res) => {
|
|
1592
1644
|
// Follow one redirect — validate location is http/https before following
|
|
1593
1645
|
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
|
|
1594
1646
|
const loc = res.headers.location;
|
|
@@ -1597,7 +1649,7 @@ function fetchUrl(url) {
|
|
|
1597
1649
|
res.resume();
|
|
1598
1650
|
return;
|
|
1599
1651
|
}
|
|
1600
|
-
fetchUrl(loc).then(resolve).catch(reject);
|
|
1652
|
+
fetchUrl(loc, { rejectUnauthorized }).then(resolve).catch(reject);
|
|
1601
1653
|
res.resume();
|
|
1602
1654
|
return;
|
|
1603
1655
|
}
|
|
@@ -1611,7 +1663,16 @@ function fetchUrl(url) {
|
|
|
1611
1663
|
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8', 0, 500000)));
|
|
1612
1664
|
res.on('error', reject);
|
|
1613
1665
|
});
|
|
1614
|
-
req.on('error',
|
|
1666
|
+
req.on('error', (err) => {
|
|
1667
|
+
// TLS cert failures: retry without verification (dev tool — user may be auditing
|
|
1668
|
+
// internal sites with self-signed certs or proxies that intercept TLS)
|
|
1669
|
+
const isCertErr = err.code && /CERT|SELF_SIGNED|UNABLE_TO/i.test(err.code);
|
|
1670
|
+
if (isCertErr && rejectUnauthorized) {
|
|
1671
|
+
fetchUrl(url, { rejectUnauthorized: false }).then(resolve).catch(reject);
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
reject(err);
|
|
1675
|
+
});
|
|
1615
1676
|
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
1616
1677
|
});
|
|
1617
1678
|
}
|
|
@@ -1641,13 +1702,13 @@ async function cmdAnalyzeUrl(url) {
|
|
|
1641
1702
|
blank();
|
|
1642
1703
|
fail(`Invalid URL: must start with http:// or https://`);
|
|
1643
1704
|
blank();
|
|
1644
|
-
process.exit(
|
|
1705
|
+
process.exit(1);
|
|
1645
1706
|
}
|
|
1646
1707
|
if (sanitizedUrl.length > 2048) {
|
|
1647
1708
|
blank();
|
|
1648
1709
|
fail(`URL too long (${sanitizedUrl.length} chars). Maximum is 2048.`);
|
|
1649
1710
|
blank();
|
|
1650
|
-
process.exit(
|
|
1711
|
+
process.exit(1);
|
|
1651
1712
|
}
|
|
1652
1713
|
url = sanitizedUrl;
|
|
1653
1714
|
|
|
@@ -1746,6 +1807,78 @@ function findBestContentFile(dirPath) {
|
|
|
1746
1807
|
return scored[0].path;
|
|
1747
1808
|
}
|
|
1748
1809
|
|
|
1810
|
+
// ── Save analysis to markdown ─────────────────────────────────────────────────
|
|
1811
|
+
|
|
1812
|
+
function analysisToMarkdown(result, sourceName, analyzedAt) {
|
|
1813
|
+
const grade = result.grade ?? gradeLetter(result.total_score).replace(/\x1b\[[0-9;]*m/g, '');
|
|
1814
|
+
const lines = [];
|
|
1815
|
+
lines.push(`# ContentGrade Report: ${sourceName}`);
|
|
1816
|
+
lines.push(``);
|
|
1817
|
+
lines.push(`Analyzed: ${analyzedAt} `);
|
|
1818
|
+
lines.push(`Overall score: **${result.total_score}/100 (${grade})** `);
|
|
1819
|
+
if (result.content_type) lines.push(`Content type: ${result.content_type.replace('_', ' ')}`);
|
|
1820
|
+
lines.push(``);
|
|
1821
|
+
|
|
1822
|
+
if (result.headline?.text) {
|
|
1823
|
+
lines.push(`## Headline`);
|
|
1824
|
+
lines.push(``);
|
|
1825
|
+
lines.push(`> ${result.headline.text}`);
|
|
1826
|
+
lines.push(``);
|
|
1827
|
+
lines.push(`Score: ${result.headline.score}/100`);
|
|
1828
|
+
if (result.headline.feedback) lines.push(`${result.headline.feedback}`);
|
|
1829
|
+
lines.push(``);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
if (result.one_line_verdict) {
|
|
1833
|
+
lines.push(`## Verdict`);
|
|
1834
|
+
lines.push(``);
|
|
1835
|
+
lines.push(`${result.one_line_verdict}`);
|
|
1836
|
+
lines.push(``);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
if (result.dimensions) {
|
|
1840
|
+
lines.push(`## Dimension Breakdown`);
|
|
1841
|
+
lines.push(``);
|
|
1842
|
+
for (const [, dim] of Object.entries(result.dimensions)) {
|
|
1843
|
+
lines.push(`**${dim.label}**: ${dim.score}/100`);
|
|
1844
|
+
if (dim.feedback) lines.push(`${dim.feedback}`);
|
|
1845
|
+
lines.push(``);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
if (result.strengths?.length) {
|
|
1850
|
+
lines.push(`## Strengths`);
|
|
1851
|
+
lines.push(``);
|
|
1852
|
+
for (const s of result.strengths) lines.push(`- ${s}`);
|
|
1853
|
+
lines.push(``);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
if (result.improvements?.length) {
|
|
1857
|
+
lines.push(`## Top Improvements`);
|
|
1858
|
+
lines.push(``);
|
|
1859
|
+
for (const imp of result.improvements) {
|
|
1860
|
+
const pri = imp.priority === 'high' ? '🔴' : '🟡';
|
|
1861
|
+
lines.push(`### ${pri} ${imp.issue}`);
|
|
1862
|
+
lines.push(``);
|
|
1863
|
+
lines.push(`**Fix:** ${imp.fix}`);
|
|
1864
|
+
lines.push(``);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
if (result.headline_rewrites?.length) {
|
|
1869
|
+
lines.push(`## Headline Rewrites`);
|
|
1870
|
+
lines.push(``);
|
|
1871
|
+
for (let i = 0; i < result.headline_rewrites.length; i++) {
|
|
1872
|
+
lines.push(`${i + 1}. ${result.headline_rewrites[i]}`);
|
|
1873
|
+
}
|
|
1874
|
+
lines.push(``);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
lines.push(`---`);
|
|
1878
|
+
lines.push(`Generated by [ContentGrade](https://buy.stripe.com/5kQeVfew48dT7nf2W48k801) v${_version}`);
|
|
1879
|
+
return lines.join('\n');
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1749
1882
|
// ── Router ────────────────────────────────────────────────────────────────────
|
|
1750
1883
|
|
|
1751
1884
|
const _rawArgs = process.argv.slice(2);
|
|
@@ -1757,10 +1890,17 @@ const _threshIdx = _rawArgs.indexOf('--threshold');
|
|
|
1757
1890
|
const _ciThreshold = (_threshIdx !== -1 && _rawArgs[_threshIdx + 1])
|
|
1758
1891
|
? (parseInt(_rawArgs[_threshIdx + 1], 10) || 60)
|
|
1759
1892
|
: 60;
|
|
1893
|
+
const _saveIdx = _rawArgs.indexOf('--save');
|
|
1894
|
+
const _saveMode = _saveIdx !== -1;
|
|
1895
|
+
const _savePath = (_saveMode && _rawArgs[_saveIdx + 1] && !_rawArgs[_saveIdx + 1].startsWith('-'))
|
|
1896
|
+
? _rawArgs[_saveIdx + 1]
|
|
1897
|
+
: null;
|
|
1898
|
+
const _demoMode = _rawArgs.includes('--demo');
|
|
1760
1899
|
const args = _rawArgs.filter((a, i) => {
|
|
1761
|
-
if (['--no-telemetry', '--json', '--quiet', '--verbose', '--ci'].includes(a)) return false;
|
|
1900
|
+
if (['--no-telemetry', '--json', '--quiet', '--verbose', '--ci', '--save', '--demo'].includes(a)) return false;
|
|
1762
1901
|
if (a === '--threshold') return false;
|
|
1763
1902
|
if (i > 0 && _rawArgs[i - 1] === '--threshold') return false;
|
|
1903
|
+
if (_saveMode && i > 0 && _rawArgs[i - 1] === '--save' && !a.startsWith('-')) return false;
|
|
1764
1904
|
return true;
|
|
1765
1905
|
});
|
|
1766
1906
|
const raw = args[0];
|
|
@@ -1779,7 +1919,119 @@ function looksLikePath(s) {
|
|
|
1779
1919
|
try { statSync(resolve(process.cwd(), s)); return true; } catch { return false; }
|
|
1780
1920
|
}
|
|
1781
1921
|
|
|
1782
|
-
|
|
1922
|
+
// Per-command --help: "content-grade <cmd> --help" → command-specific help
|
|
1923
|
+
if (_rawArgs.includes('--help') || _rawArgs.includes('-h')) {
|
|
1924
|
+
switch (cmd) {
|
|
1925
|
+
case 'analyze':
|
|
1926
|
+
case 'analyse':
|
|
1927
|
+
case 'check':
|
|
1928
|
+
case 'grade':
|
|
1929
|
+
blank();
|
|
1930
|
+
console.log(` ${B}content-grade analyze <file>${R} ${D}(aliases: grade, check)${R}`);
|
|
1931
|
+
blank();
|
|
1932
|
+
console.log(` Analyzes written content and returns an AI-powered score (0–100) with`);
|
|
1933
|
+
console.log(` dimension breakdown, specific improvements, and headline rewrites.`);
|
|
1934
|
+
blank();
|
|
1935
|
+
console.log(` ${B}Arguments:${R}`);
|
|
1936
|
+
console.log(` ${CY}<file>${R} Path to a .md, .txt, or .mdx file`);
|
|
1937
|
+
blank();
|
|
1938
|
+
console.log(` ${B}Flags:${R}`);
|
|
1939
|
+
console.log(` ${CY}--demo${R} Run on built-in sample content (no file needed)`);
|
|
1940
|
+
console.log(` ${CY}--json${R} Output raw JSON (great for CI pipelines)`);
|
|
1941
|
+
console.log(` ${CY}--quiet${R} Output score number only (for scripting)`);
|
|
1942
|
+
console.log(` ${CY}--ci${R} Exit 0 if score passes, 1 if it fails`);
|
|
1943
|
+
console.log(` ${CY}--threshold <n>${R} Score threshold for --ci (default: 60)`);
|
|
1944
|
+
console.log(` ${CY}--verbose${R} Show debug info: model, timing, response length`);
|
|
1945
|
+
blank();
|
|
1946
|
+
console.log(` ${B}Examples:${R}`);
|
|
1947
|
+
console.log(` ${CY}content-grade analyze --demo${R} ${D}# live demo on sample content${R}`);
|
|
1948
|
+
console.log(` ${CY}content-grade analyze ./blog-post.md${R}`);
|
|
1949
|
+
console.log(` ${CY}content-grade analyze ./readme.md --json${R}`);
|
|
1950
|
+
console.log(` ${CY}content-grade analyze ./copy.md --ci --threshold 70${R}`);
|
|
1951
|
+
console.log(` ${D}# store score in a variable${R}`);
|
|
1952
|
+
console.log(` ${CY}score=\$(content-grade analyze ./post.md --quiet)${R}`);
|
|
1953
|
+
blank();
|
|
1954
|
+
process.exit(0);
|
|
1955
|
+
break;
|
|
1956
|
+
case 'headline':
|
|
1957
|
+
blank();
|
|
1958
|
+
console.log(` ${B}content-grade headline "<text>"${R}`);
|
|
1959
|
+
blank();
|
|
1960
|
+
console.log(` Grades a single headline using 4 direct response copywriting frameworks:`);
|
|
1961
|
+
console.log(` Rule of One, Value Equation, Readability, and Proof/Promise.`);
|
|
1962
|
+
console.log(` Returns a 0–100 score, verdict, and 2 stronger rewrites. Takes ~5 seconds.`);
|
|
1963
|
+
blank();
|
|
1964
|
+
console.log(` ${B}Arguments:${R}`);
|
|
1965
|
+
console.log(` ${CY}"<text>"${R} The headline to grade (quote it to avoid shell expansion)`);
|
|
1966
|
+
blank();
|
|
1967
|
+
console.log(` ${B}Flags:${R}`);
|
|
1968
|
+
console.log(` ${CY}--json${R} Output raw JSON`);
|
|
1969
|
+
console.log(` ${CY}--quiet${R} Output score number only`);
|
|
1970
|
+
console.log(` ${CY}--verbose${R} Show debug info`);
|
|
1971
|
+
blank();
|
|
1972
|
+
console.log(` ${B}Examples:${R}`);
|
|
1973
|
+
console.log(` ${CY}content-grade headline "The 5-Minute Fix That Doubles Landing Page Conversions"${R}`);
|
|
1974
|
+
console.log(` ${CY}content-grade headline "How We Grew From 0 to 10k Users" --json${R}`);
|
|
1975
|
+
blank();
|
|
1976
|
+
process.exit(0);
|
|
1977
|
+
break;
|
|
1978
|
+
case 'seo-audit':
|
|
1979
|
+
case 'seoaudit':
|
|
1980
|
+
blank();
|
|
1981
|
+
console.log(` ${B}content-grade seo-audit <url>${R}`);
|
|
1982
|
+
blank();
|
|
1983
|
+
console.log(` Fetches a live URL, extracts readable content, and runs the same full AI`);
|
|
1984
|
+
console.log(` analysis as the analyze command. Good for auditing competitors or your own pages.`);
|
|
1985
|
+
blank();
|
|
1986
|
+
console.log(` ${B}Arguments:${R}`);
|
|
1987
|
+
console.log(` ${CY}<url>${R} Full URL starting with http:// or https://`);
|
|
1988
|
+
blank();
|
|
1989
|
+
console.log(` ${B}Flags:${R}`);
|
|
1990
|
+
console.log(` ${CY}--json${R} Output raw JSON`);
|
|
1991
|
+
console.log(` ${CY}--quiet${R} Output score number only`);
|
|
1992
|
+
console.log(` ${CY}--ci${R} Exit 0/1 based on score threshold`);
|
|
1993
|
+
console.log(` ${CY}--threshold <n>${R} Score threshold for --ci (default: 60)`);
|
|
1994
|
+
blank();
|
|
1995
|
+
console.log(` ${B}Examples:${R}`);
|
|
1996
|
+
console.log(` ${CY}content-grade seo-audit https://yoursite.com/blog-post${R}`);
|
|
1997
|
+
console.log(` ${CY}content-grade seo-audit https://example.com --json${R}`);
|
|
1998
|
+
blank();
|
|
1999
|
+
process.exit(0);
|
|
2000
|
+
break;
|
|
2001
|
+
case 'batch':
|
|
2002
|
+
blank();
|
|
2003
|
+
console.log(` ${B}content-grade batch <directory>${R} ${MG}[Pro]${R}`);
|
|
2004
|
+
blank();
|
|
2005
|
+
console.log(` Analyzes all .md, .txt, and .mdx files in a directory and prints a sorted`);
|
|
2006
|
+
console.log(` summary with scores. Requires a Pro license.`);
|
|
2007
|
+
blank();
|
|
2008
|
+
console.log(` ${B}Arguments:${R}`);
|
|
2009
|
+
console.log(` ${CY}<directory>${R} Path to a directory containing content files`);
|
|
2010
|
+
blank();
|
|
2011
|
+
console.log(` ${B}Flags:${R}`);
|
|
2012
|
+
console.log(` ${CY}--json${R} Output results as a JSON array (replaces styled output)`);
|
|
2013
|
+
blank();
|
|
2014
|
+
console.log(` ${B}Examples:${R}`);
|
|
2015
|
+
console.log(` ${CY}content-grade batch ./posts${R}`);
|
|
2016
|
+
console.log(` ${CY}content-grade batch ./content --json${R}`);
|
|
2017
|
+
blank();
|
|
2018
|
+
process.exit(0);
|
|
2019
|
+
break;
|
|
2020
|
+
default:
|
|
2021
|
+
cmdHelp();
|
|
2022
|
+
process.exit(0);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
if (_demoMode) {
|
|
2027
|
+
recordEvent({ event: 'command', command: 'demo' });
|
|
2028
|
+
cmdDemo().catch(err => {
|
|
2029
|
+
blank();
|
|
2030
|
+
fail(`Demo error: ${err.message}`);
|
|
2031
|
+
blank();
|
|
2032
|
+
process.exit(1);
|
|
2033
|
+
});
|
|
2034
|
+
} else switch (cmd) {
|
|
1783
2035
|
case 'analyze':
|
|
1784
2036
|
case 'analyse':
|
|
1785
2037
|
case 'check':
|
|
@@ -1851,7 +2103,7 @@ switch (cmd) {
|
|
|
1851
2103
|
console.log(` ${B}Usage:${R} ${CY}content-grade seo-audit <url>${R}`);
|
|
1852
2104
|
console.log(` ${B}Example:${R} ${CY}content-grade seo-audit https://yoursite.com/blog-post${R}`);
|
|
1853
2105
|
blank();
|
|
1854
|
-
process.exit(
|
|
2106
|
+
process.exit(1);
|
|
1855
2107
|
}
|
|
1856
2108
|
cmdAnalyzeUrl(args[1]).catch(err => {
|
|
1857
2109
|
blank();
|
package/package.json
CHANGED