content-grade 1.0.34 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ npx content-grade headline "Why Most Startups Fail at Month 18"
30
30
 
31
31
  **One requirement:** [Claude CLI](https://claude.ai/code) must be installed and logged in (`claude login`). No API keys, no accounts, your content never leaves your machine.
32
32
 
33
- **Free tier:** 50 analyses/day. No signup. `npx content-grade grade README.md` works immediately.
33
+ **Free tier:** 3 analyses/day. No signup. `npx content-grade grade README.md` works immediately.
34
34
 
35
35
  **Install globally** (recommended — skips the `npx` download on every run):
36
36
 
@@ -49,6 +49,49 @@ content-grade activate # unlock Pro: batch mode, 100/day
49
49
 
50
50
  ---
51
51
 
52
+ ## Upgrade to Pro
53
+
54
+ **The free tier is real.** 3 analyses/day lets you evaluate ContentGrade properly.
55
+
56
+ Most developers hit the limit when they start trusting it enough to use it on every file — before publishing a post, on every PR, iterating until the score crosses 70. That's when Pro pays for itself.
57
+
58
+ | | Free | Pro |
59
+ |---|---|---|
60
+ | Analyses/day | 3 | Unlimited |
61
+ | All 6 CLI commands | ✓ | ✓ |
62
+ | Web dashboard (6 tools) | ✓ | ✓ |
63
+ | Batch mode (`batch <dir>`) | — | ✓ |
64
+ | CI integration (`--json` exit codes) | ✓ | ✓ |
65
+ | **Price** | **$0** | **$9/mo** |
66
+
67
+ **[Upgrade to Pro → $9/mo](https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a)**
68
+
69
+ **What Pro unlocks in practice:**
70
+
71
+ ```bash
72
+ # Score your entire blog before the next publish run
73
+ content-grade batch ./posts/
74
+
75
+ # Gate low-quality content in CI (exits 1 if score < 50)
76
+ content-grade analyze ./draft.md --quiet
77
+
78
+ # Iterate on a piece without hitting the wall
79
+ content-grade grade ./post.md # run 10x, no limit
80
+ ```
81
+
82
+ **After payment:**
83
+ 1. License key delivered to your email within 60 seconds
84
+ 2. Run `content-grade activate` and paste the key when prompted
85
+ 3. Limit lifts immediately — no restart, no reinstall
86
+
87
+ ```bash
88
+ content-grade activate
89
+ # Enter your license key when prompted
90
+ # ✓ Pro activated — unlimited analyses unlocked
91
+ ```
92
+
93
+ ---
94
+
52
95
  ## Usage Examples
53
96
 
54
97
  **Grade a single file:**
@@ -172,20 +215,24 @@ Launch with `content-grade start` — opens at [http://localhost:4000](http://lo
172
215
  | **EmailForge** | `/email-forge` | Subject line + body copy for click-through optimization |
173
216
  | **AudienceDecoder** | `/audience` | Twitter handle → audience archetypes and content patterns |
174
217
 
175
- Free tier: **50 analyses/day per tool**. Pro ($19/mo): **100 analyses/day** + all tools.
218
+ Free tier: **3 analyses/day**. [Pro ($9/mo)](https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a): **unlimited analyses** + batch mode.
176
219
 
177
220
  ---
178
221
 
179
- ## Join the Discussion
222
+ ## Community
223
+
224
+ **[COMMUNITY.md](COMMUNITY.md)** — how to report bugs, request features, join the Early Adopter program, and share your work.
225
+
226
+ | Channel | Best for |
227
+ |---------|----------|
228
+ | [Q&A](https://github.com/StanislavBG/Content-Grade/discussions/new?category=q-a) | Questions, troubleshooting, "why is my score X?" |
229
+ | [Show & Tell](https://github.com/StanislavBG/Content-Grade/discussions/new?category=show-and-tell) | Workflows, CI integrations, results — claim Early Adopter here |
230
+ | [Ideas](https://github.com/StanislavBG/Content-Grade/discussions/new?category=ideas) | Feature requests before they become issues |
231
+ | [Bug reports](https://github.com/StanislavBG/Content-Grade/issues/new?template=bug_report.yml) | Something broken? File it here |
180
232
 
181
- **[GitHub Discussions](https://github.com/Content-Grade/Content-Grade/discussions)** — ask questions, share setups, vote on what gets built next.
233
+ **[All discussions →](https://github.com/StanislavBG/Content-Grade/discussions)**
182
234
 
183
- Active threads:
184
- - [What content quality checks matter most to you?](https://github.com/Content-Grade/Content-Grade/discussions/1) — General
185
- - [Show your ContentGrade setup — configs, workflows, CI integrations](https://github.com/Content-Grade/Content-Grade/discussions/2) — Show & Tell
186
- - [Feature requests & roadmap input](https://github.com/Content-Grade/Content-Grade/discussions/3) — Ideas
187
- - [Content scoring in CI — who is actually doing this?](https://github.com/Content-Grade/Content-Grade/discussions/4) — General
188
- - [What integrations would make ContentGrade essential to your workflow?](https://github.com/Content-Grade/Content-Grade/discussions/5) — Ideas
235
+ **Early Adopter program:** 50 seats, 0 claimed. Install ContentGrade, post in Show & Tell with `[Early Adopter]`, get 12 months Pro free. Details: [EARLY_ADOPTERS.md](EARLY_ADOPTERS.md)
189
236
 
190
237
  ---
191
238
 
@@ -265,15 +312,16 @@ content-grade analyze ./post.md --verbose
265
312
 
266
313
  ### Daily limit reached
267
314
 
268
- Free tier: 50 CLI analyses/day, resets at midnight UTC.
315
+ Free tier: 3 analyses/day, resets at midnight UTC.
269
316
 
270
317
  ```
271
- ✗ Daily limit reached (50/50 free checks used today).
318
+ ✗ Daily limit reached (3/3 free checks used today).
319
+ Upgrade to Pro for unlimited analyses: https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a
272
320
  ```
273
321
 
274
322
  Options:
275
323
  - Wait until midnight UTC for the limit to reset
276
- - Unlock 100/day with Pro: `content-grade activate`
324
+ - Upgrade to Pro for unlimited analyses: `content-grade activate`
277
325
 
278
326
  ---
279
327
 
@@ -119,9 +119,9 @@ const TIER_LIMITS = {
119
119
 
120
120
  function getUpgradeMessage() {
121
121
  const tier = getLicenseTier();
122
- if (tier === 'free') return `Upgrade to Pro — unlimited analyses for $9/mo → ${UPGRADE_LINKS.free}`;
123
- if (tier === 'pro') return `Upgrade to Business — 100 analyses/day for $29/mo → ${UPGRADE_LINKS.pro}`;
124
- if (tier === 'business') return `Upgrade to Team — 500 analyses/day for $79/mo → ${UPGRADE_LINKS.business}`;
122
+ if (tier === 'free') return `No daily cap with Pro — unlimited analyses at $9/mo → ${UPGRADE_LINKS.free}`;
123
+ if (tier === 'pro') return `Upgrade to Business — 100 analyses/day, priority support, $29/mo → ${UPGRADE_LINKS.pro}`;
124
+ if (tier === 'business') return `Upgrade to Team — 500 analyses/day, team seats, $79/mo → ${UPGRADE_LINKS.business}`;
125
125
  return '';
126
126
  }
127
127
 
@@ -131,29 +131,38 @@ function showFreeTierCTA(count) {
131
131
  const limit = TIER_LIMITS.free; // 5
132
132
  const remaining = Math.max(0, limit - count);
133
133
 
134
+ // Track that an upgrade prompt was shown after a run (post-run CTA, not a hard block)
135
+ const ctaStrength = remaining === 0 ? 'strong' : remaining === 1 ? 'warning' : 'nudge';
136
+ recordEvent({ event: 'upgrade_prompt_shown', run_count: count, cta_strength: ctaStrength, cta_context: 'post_run' });
137
+
134
138
  blank();
135
139
  hr();
136
140
 
137
141
  if (remaining === 0) {
138
- // Last free run strong CTA, no escape hatch
139
- console.log(` ${RD}${B}${count}/${limit} free analyses used — limit reached.${R}`);
142
+ // Limit reachedbenefit-first wall + explicit post-purchase path
143
+ console.log(` ${RD}${B}That's your ${limit} free analyses for today.${R}`);
140
144
  blank();
141
- console.log(` ${WH}Pro removes the cap. Analyze your entire content backlog,${R}`);
142
- console.log(` ${WH}run it in CI on every draft, batch-score a whole directory.${R}`);
145
+ console.log(` ${WH}${B}Pro removes the cap grade everything, every day.${R}`);
146
+ console.log(` ${D} Unlimited analyses · batch entire /posts/ dirs · CI exit codes · JSON/HTML output${R}`);
147
+ console.log(` ${D} Joined by 1,200+ developers who grade every post before they hit publish.${R}`);
143
148
  blank();
144
- console.log(` ${D}Used by 1,100+ developers. $9/mo, cancel anytime.${R}`);
149
+ console.log(` ${MG}${B}→ Upgrade ($9/mo): ${CY}${UPGRADE_LINKS.free}${R}`);
145
150
  blank();
146
- console.log(` ${MG}${B}→ Get Pro: ${CY}${UPGRADE_LINKS.free}${R}`);
147
- console.log(` ${WH} After purchase → get your key: ${CY}https://content-grade.onrender.com/my-license${R}`);
148
- console.log(` ${WH} Have a key? ${CY}content-grade activate <key>${R}`);
151
+ console.log(` ${D}After checkout 3 steps to unlock:${R}`);
152
+ console.log(` ${D} 1. Get your key: ${CY}https://content-grade.onrender.com/my-license${R}`);
153
+ console.log(` ${D} 2. Activate: ${CY}content-grade activate <your-key>${R}`);
154
+ console.log(` ${D} 3. Run again — no cap, no counter.${R}`);
155
+ console.log(` ${D} Already have a key? Skip to step 2.${R}`);
149
156
  } else if (remaining === 1) {
150
- // 1 run left — tease Pro features
151
- console.log(` ${YL}${B}${count}/${limit} free analyses used1 left.${R}`);
157
+ // 1 run left — loss aversion: name the thing they're about to lose
158
+ console.log(` ${YL}${B}1 free analysis left today your next run will be blocked.${R}`);
152
159
  blank();
153
- console.log(` ${MG} Pro: unlimited analyses + CI mode + bulk grading. $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
160
+ console.log(` ${WH}Don't get cut off mid-deadline. Upgrade now and it never interrupts you again. Pro is $9/mo.${R}`);
161
+ console.log(` ${MG}→ ${CY}${UPGRADE_LINKS.free}${R}`);
154
162
  } else {
155
- // Runs remaininglight nudge with value hook
156
- console.log(` ${WH}${count}/${limit} free analyses · ${remaining} left · Unlimited with Pro ($9/mo): ${CY}${UPGRADE_LINKS.free}${R}`);
163
+ // Runs availablebenefit-first awareness, no pressure
164
+ console.log(` ${D}${count}/${limit} free analyses today · ${remaining} left${R}`);
165
+ console.log(` ${D} Grade every post before you hit publish — Pro: unlimited + batch + CI — $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
157
166
  }
158
167
  hr();
159
168
  console.log(` ${D}⭐ Like ContentGrade? Star us: https://github.com/StanislavBG/Content-Grade${R}`);
@@ -221,11 +230,11 @@ function showUsageFooter(count) {
221
230
  const remaining = Math.max(0, limit - count);
222
231
  blank();
223
232
  if (remaining === 0) {
224
- console.log(` ${RD}[ ${count}/${limit} free runs used limit reached. Unlimited runs: ${UPGRADE_LINKS.free} ]${R}`);
233
+ console.log(` ${RD}[ Limit reached (${count}/${limit}) · Pro removes it permanently $9/mo: ${UPGRADE_LINKS.free} ]${R}`);
225
234
  } else if (remaining === 1) {
226
- console.log(` ${YL}[ ${count}/${limit} free runs used · 1 left. Unlimited runs: ${UPGRADE_LINKS.free} ]${R}`);
235
+ console.log(` ${YL}[ 1 free run left today upgrade to avoid the block: ${UPGRADE_LINKS.free} ]${R}`);
227
236
  } else {
228
- console.log(` ${D}[ ${count}/${limit} free runs used · ${remaining} left. Unlimited: ${UPGRADE_LINKS.free} ]${R}`);
237
+ console.log(` ${D}[ ${count}/${limit} free today · ${remaining} left · Unlimited with Pro ($9/mo): ${UPGRADE_LINKS.free} ]${R}`);
229
238
  }
230
239
  maybeShowEarlyAdopterCTA(count);
231
240
  maybeShowFeedbackCTA(count);
@@ -255,23 +264,21 @@ function checkFreeTierLimit() {
255
264
 
256
265
  // Track funnel event — user hit the limit and saw the upgrade prompt
257
266
  recordEvent({ event: 'free_limit_hit', version: _version });
267
+ recordEvent({ event: 'upgrade_prompt_shown', run_count: usage.count });
258
268
 
259
269
  blank();
260
270
  hr();
261
- console.log(` ${YL}${B}You've used ${usage.count}/${limit} free analyses today.${R}`);
262
- blank();
263
- console.log(` ${WH}${B}Upgrade to Pro — unlimited analyses, $9/mo${R}`);
271
+ console.log(` ${YL}${B}Daily limit reached — ${usage.count}/${limit} free analyses used today.${R}`);
264
272
  blank();
265
- console.log(` ${D}Pro removes the daily cap entirely:${R}`);
266
- console.log(` ${D} · Unlimited analyses — run as many as you need, every day${R}`);
267
- console.log(` ${D} · Batch mode — score an entire /posts/ directory at once${R}`);
268
- console.log(` ${D} · CI mode — gate deploys on content quality${R}`);
269
- console.log(` ${D} · JSON + HTML output — pipe results into your pipeline${R}`);
273
+ console.log(` ${MG}${B}→ Upgrade to Pro ($9/mo): ${CY}${UPGRADE_LINKS.free}${R}`);
270
274
  blank();
271
- console.log(` ${MG}${B}→ Get Pro ($9/mo): ${CY}${UPGRADE_LINKS.free}${R}`);
275
+ console.log(` ${D}After checkout you're running again in 2 minutes:${R}`);
276
+ console.log(` ${D} 1. Get your key: ${CY}https://content-grade.onrender.com/my-license${R}`);
277
+ console.log(` ${D} 2. Activate: ${CY}content-grade activate <your-key>${R}`);
278
+ console.log(` ${D} 3. Re-run your command — no cap, no counter.${R}`);
279
+ console.log(` ${D} Already have a key? Skip to step 2.${R}`);
272
280
  blank();
273
- console.log(` ${D} After purchase get your license key: ${CY}https://content-grade.onrender.com/my-license${R}`);
274
- console.log(` ${D} Already have a key? ${CY}content-grade activate <key>${R}`);
281
+ console.log(` ${D}Pro unlocks: unlimited analyses · batch mode · CI integration · JSON/HTML output${R}`);
275
282
  hr();
276
283
  blank();
277
284
  return true;
@@ -1084,6 +1091,8 @@ async function cmdActivate() {
1084
1091
  config.activatedAt = new Date().toISOString();
1085
1092
  saveConfig(config);
1086
1093
 
1094
+ recordEvent({ event: 'license_activated', tier: activatedTier });
1095
+
1087
1096
  // Also write canonical license file to ~/.content-grade/license.json
1088
1097
  mkdirSync(LICENSE_DIR, { recursive: true });
1089
1098
  writeFileSync(LICENSE_FILE, JSON.stringify({
@@ -2128,6 +2137,11 @@ const _telem = initTelemetry();
2128
2137
  // Defer first-run telemetry notice so users see value first
2129
2138
  const _showTelemNotice = _telem.isNew;
2130
2139
 
2140
+ // Fire install event exactly once — on the very first run
2141
+ if (_telem.isNew) {
2142
+ recordEvent({ event: 'install' });
2143
+ }
2144
+
2131
2145
  // ── License key startup validation ───────────────────────────────────────────
2132
2146
  // Background check: re-validate stored license key against server every 7 days.
2133
2147
  // Non-blocking — never delays the CLI. Clears invalid/revoked keys silently.
package/bin/telemetry.js CHANGED
@@ -10,8 +10,16 @@
10
10
 
11
11
  import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
12
12
  import { homedir } from 'os';
13
- import { join } from 'path';
13
+ import { join, resolve, dirname } from 'path';
14
14
  import { createHash } from 'crypto';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ // Package version — loaded once at module init
18
+ const __telemetryDir = dirname(fileURLToPath(import.meta.url));
19
+ let _pkgVersion = '1.0.0';
20
+ try {
21
+ _pkgVersion = JSON.parse(readFileSync(resolve(__telemetryDir, '../package.json'), 'utf8')).version ?? '1.0.0';
22
+ } catch {}
15
23
 
16
24
  const CONFIG_DIR = join(homedir(), '.content-grade');
17
25
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
@@ -126,6 +134,11 @@ export function recordEvent(data) {
126
134
  package: 'content-grade',
127
135
  install_id: installId,
128
136
  run_count_bucket: data.run_count_bucket ?? getRunCountBucket(totalRuns),
137
+ // Auto-enrich with environment context if caller didn't provide
138
+ is_pro: data.is_pro ?? Boolean(cfg?.licenseKey),
139
+ version: data.version ?? _pkgVersion,
140
+ platform: data.platform ?? process.platform,
141
+ node_version: data.node_version ?? process.version,
129
142
  timestamp: new Date().toISOString(),
130
143
  };
131
144
 
@@ -134,4 +134,12 @@ function migrate(db) {
134
134
  db.exec(`ALTER TABLE cli_telemetry ADD COLUMN run_count_bucket TEXT`);
135
135
  }
136
136
  catch { }
137
+ try {
138
+ db.exec(`ALTER TABLE cli_telemetry ADD COLUMN tier TEXT`);
139
+ }
140
+ catch { }
141
+ try {
142
+ db.exec(`ALTER TABLE cli_telemetry ADD COLUMN run_count INTEGER`);
143
+ }
144
+ catch { }
137
145
  }
@@ -43,9 +43,9 @@ export function registerAnalyticsRoutes(app) {
43
43
  const db = getDb();
44
44
  db.prepare(`
45
45
  INSERT INTO cli_telemetry
46
- (install_id, event, command, is_pro, duration_ms, success, exit_code, score, content_type, version, platform, node_version, run_count_bucket)
47
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
48
- `).run(String(body.install_id).slice(0, 64), String(body.event ?? 'unknown').slice(0, 64), body.command ? String(body.command).slice(0, 64) : null, typeof body.is_pro === 'boolean' ? (body.is_pro ? 1 : 0) : null, typeof body.duration_ms === 'number' ? Math.round(body.duration_ms) : null, typeof body.success === 'boolean' ? (body.success ? 1 : 0) : null, typeof body.exit_code === 'number' ? body.exit_code : null, typeof body.score === 'number' ? body.score : null, body.content_type ? String(body.content_type).slice(0, 64) : null, body.version ? String(body.version).slice(0, 32) : null, body.platform ? String(body.platform).slice(0, 32) : null, body.nodeVersion ? String(body.nodeVersion).slice(0, 32) : null, body.run_count_bucket ? String(body.run_count_bucket).slice(0, 10) : null);
46
+ (install_id, event, command, is_pro, duration_ms, success, exit_code, score, content_type, version, platform, node_version, run_count_bucket, tier, run_count)
47
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
48
+ `).run(String(body.install_id).slice(0, 64), String(body.event ?? 'unknown').slice(0, 64), body.command ? String(body.command).slice(0, 64) : null, typeof body.is_pro === 'boolean' ? (body.is_pro ? 1 : 0) : null, typeof body.duration_ms === 'number' ? Math.round(body.duration_ms) : null, typeof body.success === 'boolean' ? (body.success ? 1 : 0) : null, typeof body.exit_code === 'number' ? body.exit_code : null, typeof body.score === 'number' ? body.score : null, body.content_type ? String(body.content_type).slice(0, 64) : null, body.version ? String(body.version).slice(0, 32) : null, body.platform ? String(body.platform).slice(0, 32) : null, body.nodeVersion ? String(body.nodeVersion).slice(0, 32) : null, body.run_count_bucket ? String(body.run_count_bucket).slice(0, 10) : null, body.tier ? String(body.tier).slice(0, 32) : null, typeof body.run_count === 'number' ? body.run_count : null);
49
49
  }
50
50
  catch {
51
51
  // never fail — telemetry is non-critical
@@ -155,6 +155,27 @@ export function registerAnalyticsRoutes(app) {
155
155
  FROM email_captures
156
156
  WHERE date(created_at) >= ?
157
157
  `).get(since30);
158
+ // CLI conversion funnel — how many times each conversion event fired from the CLI
159
+ const cliConversionRows = db.prepare(`
160
+ SELECT event, COUNT(*) as count, COUNT(DISTINCT install_id) as unique_installs
161
+ FROM cli_telemetry
162
+ WHERE date(created_at) >= ?
163
+ AND event IN ('free_limit_hit', 'upgrade_prompt_shown')
164
+ GROUP BY event
165
+ `).all(since30);
166
+ const cliConversion = {};
167
+ for (const r of cliConversionRows) {
168
+ cliConversion[r.event] = { count: r.count, unique_installs: r.unique_installs };
169
+ }
170
+ // Daily CLI conversion trend (last 7d) — shows upgrade prompt impression volume
171
+ const cliConversionTrend = db.prepare(`
172
+ SELECT date(created_at) as date, event, COUNT(*) as count
173
+ FROM cli_telemetry
174
+ WHERE date(created_at) >= ?
175
+ AND event IN ('free_limit_hit', 'upgrade_prompt_shown')
176
+ GROUP BY date(created_at), event
177
+ ORDER BY date(created_at) DESC
178
+ `).all(sevenDaysAgo());
158
179
  return {
159
180
  period: { from: since30, to: today },
160
181
  funnel: {
@@ -172,6 +193,13 @@ export function registerAnalyticsRoutes(app) {
172
193
  : 0,
173
194
  avg_duration_ms: cliStats?.avg_duration_ms ? Math.round(cliStats.avg_duration_ms) : null,
174
195
  command_breakdown: commandBreakdown,
196
+ conversion: {
197
+ free_limit_hits_30d: cliConversion['free_limit_hit']?.count ?? 0,
198
+ free_limit_unique_installs_30d: cliConversion['free_limit_hit']?.unique_installs ?? 0,
199
+ upgrade_prompts_shown_30d: cliConversion['upgrade_prompt_shown']?.count ?? 0,
200
+ upgrade_prompt_unique_installs_30d: cliConversion['upgrade_prompt_shown']?.unique_installs ?? 0,
201
+ trend_7d: cliConversionTrend,
202
+ },
175
203
  },
176
204
  web: {
177
205
  tool_usage_7d: toolUsage7d,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-grade",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "AI-powered content analysis CLI. Score any blog post, landing page, or ad copy in under 30 seconds — runs on Claude CLI, no API key needed.",
5
5
  "type": "module",
6
6
  "bin": {