content-grade 1.0.22 → 1.0.24

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.
@@ -128,45 +128,57 @@ function showFreeTierCTA(count) {
128
128
  hr();
129
129
 
130
130
  if (remaining === 0) {
131
- // Last free run used — maximum urgency
132
- console.log(` ${RD}${B}Daily limit reached${R} ${D}(${count}/${limit} free runs used today)${R}`);
131
+ // All free runs used — reframe as success signal
132
+ console.log(` ${RD}${B}${count}/${limit} free analyses used.${R}`);
133
133
  blank();
134
- console.log(` ${B}Upgrade to Pro — $9/mo${R} to keep going:`);
134
+ console.log(` ${D}That means content-grade is working you're running it${R}`);
135
+ console.log(` ${D}consistently. Pro removes the cap and adds CI integration${R}`);
136
+ console.log(` ${D}so your pipeline runs it on every draft automatically.${R}`);
135
137
  blank();
136
- console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} no daily cap`);
137
- console.log(` ${GN}✓${R} ${B}Batch mode${R} grade an entire directory at once`);
138
- console.log(` ${GN}✓${R} ${B}URL analysis${R} audit any live page`);
139
- console.log(` ${GN}✓${R} ${B}Priority support${R} direct email response within 24h`);
140
- blank();
141
- console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
142
- console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
143
- blank();
144
- console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
138
+ console.log(` ${MG}${B}$9/mo ${CY}${UPGRADE_LINKS.free}${R}`);
139
+ console.log(` ${D}Have a key? ${CY}content-grade activate <key>${R}`);
145
140
  } else if (remaining === 1) {
146
- // 1 run left — build urgency
147
- console.log(` ${YL}${B}${count}/${limit} free runs used${R} ${D}· 1 remaining${R}`);
148
- blank();
149
- console.log(` Last free run of the day. Pro is ${B}$9/mo${R}:`);
150
- blank();
151
- console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} + batch mode + priority support`);
152
- console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
153
- console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
141
+ // 1 run left — tease Pro features
142
+ console.log(` ${YL}${B}${count}/${limit} free analyses used 1 left today.${R}`);
154
143
  blank();
155
- console.log(` ${D}Or use your last run: ${CY}content-grade analyze ./another-post.md${R}`);
144
+ console.log(` ${MG} Pro adds CI mode + bulk grading. $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
156
145
  } else {
157
- // 1+ runs left light, value-focused nudge
158
- console.log(` ${D}Free tier: ${count}/${limit} runs used today · ${remaining} remaining${R}`);
159
- blank();
160
- console.log(` ${B}What to do next:${R}`);
161
- blank();
162
- console.log(` ${CY}content-grade headline "Your title here"${R} ${D}# grade a headline in ~5s${R}`);
163
- console.log(` ${CY}content-grade analyze ./another-post.md${R} ${D}# analyze another file${R}`);
164
- console.log(` ${CY}content-grade analyze https://yoursite.com/post${R} ${D}# audit any live URL${R}`);
165
- blank();
166
- console.log(` ${MG}Unlock batch mode:${R} grade a whole directory at once`);
167
- console.log(` ${D} Have a key? ${CY}content-grade activate${R}`);
168
- console.log(` Get Pro — $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
146
+ // Runs remainingcompact nudge
147
+ console.log(` ${D}${count}/${limit} free analyses today · ${remaining} left · Unlimited: ${CY}${UPGRADE_LINKS.free}${R}`);
169
148
  }
149
+ hr();
150
+ }
151
+
152
+ // ── Early Adopter program enrollment CTA ─────────────────────────────────────
153
+
154
+ const EARLY_ADOPTER_URL = 'https://github.com/StanislavBG/Content-Grade/discussions/new?category=show-and-tell';
155
+
156
+ function hasShownEarlyAdopterCTA() {
157
+ return Boolean(loadConfig().earlyAdopterCTAShown);
158
+ }
159
+
160
+ function markEarlyAdopterCTAShown() {
161
+ const cfg = loadConfig();
162
+ cfg.earlyAdopterCTAShown = true;
163
+ saveConfig(cfg);
164
+ }
165
+
166
+ // Show once, after the first successful run, to non-Pro users who haven't seen it.
167
+ function maybeShowEarlyAdopterCTA(count) {
168
+ if (isProUser()) return;
169
+ if (count !== 1) return; // only after the very first run today
170
+ if (hasShownEarlyAdopterCTA()) return; // only once per install
171
+ markEarlyAdopterCTAShown();
172
+ blank();
173
+ hr();
174
+ console.log(` ${GN}${B}Early Adopter program — 50 founding seats, 0 claimed.${R}`);
175
+ blank();
176
+ console.log(` ${WH}You just ran ContentGrade. That qualifies you.${R}`);
177
+ console.log(` ${D}Post in Show & Tell with ${CY}[Early Adopter]${R}${D} in the title → get 12 months Pro free.${R}`);
178
+ blank();
179
+ console.log(` ${D}Claim your seat: ${CY}${EARLY_ADOPTER_URL}${R}`);
180
+ console.log(` ${D}What's included: ${CY}https://github.com/StanislavBG/Content-Grade/blob/main/EARLY_ADOPTERS.md${R}`);
181
+ hr();
170
182
  }
171
183
 
172
184
  // Single-line usage counter appended after every command run.
@@ -177,12 +189,13 @@ function showUsageFooter(count) {
177
189
  const remaining = Math.max(0, limit - count);
178
190
  blank();
179
191
  if (remaining === 0) {
180
- console.log(` ${RD}[ ${count}/${limit} free runs used today · limit reached upgrade for unlimited: ${UPGRADE_LINKS.free} ]${R}`);
192
+ console.log(` ${RD}[ ${count}/${limit} free runs used limit reached. Unlimited runs: ${UPGRADE_LINKS.free} ]${R}`);
181
193
  } else if (remaining === 1) {
182
- console.log(` ${YL}[ ${count}/${limit} free runs used today · 1 remaining upgrade for unlimited: ${UPGRADE_LINKS.free} ]${R}`);
194
+ console.log(` ${YL}[ ${count}/${limit} free runs used today · 1 left. Unlimited runs: ${UPGRADE_LINKS.free} ]${R}`);
183
195
  } else {
184
- console.log(` ${D}[ ${count}/${limit} free runs used today upgrade for unlimited: ${UPGRADE_LINKS.free} ]${R}`);
196
+ console.log(` ${D}[ ${count}/${limit} free runs today · ${remaining} left. Unlimited: ${UPGRADE_LINKS.free} ]${R}`);
185
197
  }
198
+ maybeShowEarlyAdopterCTA(count);
186
199
  }
187
200
 
188
201
  // Block a run before it starts if the free tier is exhausted.
@@ -196,19 +209,16 @@ function checkFreeTierLimit() {
196
209
 
197
210
  blank();
198
211
  hr();
199
- console.log(` ${RD}${B}Daily limit reached${R} ${D}(${usage.count}/${limit} free runs used today)${R}`);
200
- blank();
201
- console.log(` ${B}Upgrade to Pro — $9/mo${R} to keep going:`);
202
- blank();
203
- console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} no daily cap`);
204
- console.log(` ${GN}✓${R} ${B}Batch mode${R} grade an entire directory at once`);
205
- console.log(` ${GN}✓${R} ${B}URL analysis${R} audit any live page`);
206
- console.log(` ${GN}✓${R} ${B}Priority support${R} direct email response within 24h`);
212
+ console.log(` ${RD}${B} Free limit reached (${limit}/day used)${R}`);
207
213
  blank();
208
- console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
209
- console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
214
+ console.log(` ${WH}content-grade Pro — $9/mo, cancel anytime${R}`);
215
+ console.log(` ${D}├── Unlimited analyses — no daily cap${R}`);
216
+ console.log(` ${D}├── JSON + HTML output — pipe into your pipeline${R}`);
217
+ console.log(` ${D}├── CI mode — exit 1 on below-threshold content${R}`);
218
+ console.log(` ${D}└── Bulk grading — score an entire /posts/ directory${R}`);
210
219
  blank();
211
- console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
220
+ console.log(` ${MG}${B}→ Buy: ${CY}${UPGRADE_LINKS.free}${R}`);
221
+ console.log(` ${D} Already have a key? ${CY}content-grade activate <key>${R}`);
212
222
  hr();
213
223
  blank();
214
224
  return true;
@@ -646,7 +656,7 @@ async function cmdAnalyze(filePath) {
646
656
  hr();
647
657
  console.log(` ${D}Pro active · Next: ${CY}content-grade batch ./posts/${R}${D} to analyze a whole directory${R}`);
648
658
  } else {
649
- showUsageFooter(usageCount);
659
+ showFreeTierCTA(usageCount);
650
660
  }
651
661
 
652
662
  // CI exit code — shown after full output so user sees the score before exit
@@ -887,7 +897,7 @@ async function cmdInit() {
887
897
  console.log(` ${CY}content-grade start${R}`);
888
898
  blank();
889
899
  }
890
- console.log(` ${D}Pro tier: unlimited analyses/day + batch mode ${CY}content-grade.onrender.com${R}`);
900
+ console.log(` ${D}Need unlimited runs? Pro is $9/mo ${CY}${UPGRADE_LINKS.free}${R}`);
891
901
  blank();
892
902
  }
893
903
 
@@ -912,11 +922,9 @@ async function cmdActivate() {
912
922
  return;
913
923
  }
914
924
 
915
- console.log(` ${D}Pro tier unlocks:${R}`);
916
- console.log(` ${D} · ${CY}content-grade batch <dir>${R}${D} — analyze entire directories${R}`);
917
- console.log(` ${D} · Unlimited analyses/day (vs 3 free)${R}`);
925
+ console.log(` ${D}Pro unlocks unlimited runs/day + batch mode for $9/mo:${R}`);
918
926
  blank();
919
- console.log(` ${D}Get a license: ${CY}content-grade.onrender.com${R}${D} → Pricing${R}`);
927
+ console.log(` ${MG}${B}→ Get Pro: ${CY}${UPGRADE_LINKS.free}${R}`);
920
928
  blank();
921
929
 
922
930
  let key;
@@ -997,7 +1005,7 @@ async function cmdActivate() {
997
1005
  } else {
998
1006
  process.stdout.write('\n');
999
1007
  blank();
1000
- fail(data.message || 'Invalid or expired license key. Visit content-grade.onrender.com to check your subscription.');
1008
+ fail(data.message || `Invalid or expired license key. Get a new key: ${UPGRADE_LINKS.free}`);
1001
1009
  blank();
1002
1010
  process.exit(1);
1003
1011
  }
@@ -1026,8 +1034,9 @@ async function cmdActivate() {
1026
1034
  }, null, 2), 'utf8');
1027
1035
 
1028
1036
  const tierLimit = TIER_LIMITS[activatedTier] || TIER_LIMITS.free;
1037
+ const tierLimitDisplay = tierLimit === Infinity ? 'unlimited' : `${tierLimit}`;
1029
1038
  blank();
1030
- ok(`${(TIER_NAMES[activatedTier] || activatedTier).split(' —')[0]} activated! Your daily limit is now ${tierLimit} analyses.`);
1039
+ ok(`${(TIER_NAMES[activatedTier] || activatedTier).split(' —')[0]} activated! Your daily limit is now ${tierLimitDisplay} analyses.`);
1031
1040
  blank();
1032
1041
  console.log(` ${B}Try it:${R}`);
1033
1042
  console.log(` ${CY} content-grade batch ./posts/${R} ${D}# analyze all files${R}`);
@@ -1041,21 +1050,13 @@ async function cmdBatch(dirPath) {
1041
1050
  blank();
1042
1051
  console.log(` ${B}${MG}Batch Analysis — Pro Feature${R}`);
1043
1052
  blank();
1044
- console.log(` ${D}Batch mode requires a Pro license.${R}`);
1045
- blank();
1046
- console.log(` ${D}Free tier: analyze files one at a time.${R}`);
1047
- console.log(` ${CY}content-grade analyze ./post.md${R}`);
1048
- blank();
1049
- console.log(` ${B}Upgrade to Pro — $9/mo${R} to unlock batch mode:`);
1053
+ console.log(` ${D}Batch mode grades every file in a directory in one shot — Pro only.${R}`);
1050
1054
  blank();
1051
- console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} no daily cap`);
1052
- console.log(` ${GN}✓${R} ${B}Batch mode${R} grade an entire directory at once`);
1053
- console.log(` ${GN}✓${R} ${B}Priority support${R} direct email response within 24h`);
1055
+ console.log(` Pro is $9/mo and also unlocks unlimited daily runs.`);
1054
1056
  blank();
1055
- console.log(` ${MG}${B}→ Upgrade to Pro $9/mo${R}`);
1056
- console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
1057
+ console.log(` ${MG}${B}→ Get Pro: ${CY}${UPGRADE_LINKS.free}${R}`);
1057
1058
  blank();
1058
- console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
1059
+ console.log(` ${D}Already have a key? ${CY}content-grade activate <key>${R}`);
1059
1060
  blank();
1060
1061
  process.exit(1);
1061
1062
  }
@@ -1263,7 +1264,7 @@ function cmdStart() {
1263
1264
  info(` EmailForge — ${url}/email-forge`);
1264
1265
  info(` AudienceDecoder — ${url}/audience`);
1265
1266
  blank();
1266
- info(`Free tier: 5 analyses/day. Upgrade at ${url}`);
1267
+ info(`Free tier: 3 analyses/day. Unlimited with Pro ($9/mo) → ${UPGRADE_LINKS.free}`);
1267
1268
  info(`Press Ctrl+C to stop`);
1268
1269
  blank();
1269
1270
  openBrowser(url);
@@ -1438,7 +1439,7 @@ async function cmdMetrics() {
1438
1439
  function cmdHelp() {
1439
1440
  banner();
1440
1441
  console.log(` ${D}AI-powered content quality scoring — readability, SEO, structure analysis${R}`);
1441
- console.log(` ${D}v${_version} · ${CY}content-grade.onrender.com${R}`);
1442
+ console.log(` ${D}v${_version} · ${CY}npmjs.com/package/content-grade${R}`);
1442
1443
  blank();
1443
1444
  console.log(` ${B}QUICK START${R}`);
1444
1445
  blank();
@@ -1539,8 +1540,8 @@ function cmdHelp() {
1539
1540
  console.log(` Business 100/day $29/mo`);
1540
1541
  console.log(` Team 500/day $79/mo`);
1541
1542
  blank();
1542
- console.log(` Purchase at: ${CY}content-grade.onrender.com${R}`);
1543
- console.log(` Then unlock: ${CY}content-grade activate <your-license-key>${R}`);
1543
+ console.log(` Get Pro: ${CY}${UPGRADE_LINKS.free}${R} ${D}# direct checkout${R}`);
1544
+ console.log(` Activate: ${CY}content-grade activate <your-license-key>${R}`);
1544
1545
  blank();
1545
1546
  }
1546
1547
 
package/dist/landing.html CHANGED
@@ -36,7 +36,7 @@
36
36
  "@type": "Offer",
37
37
  "price": "0",
38
38
  "priceCurrency": "USD",
39
- "description": "Free: 3 analyses/day. Pro: $9/mo, 20/day. Business: $29/mo, 100/day. Team: $79/mo, 500/day."
39
+ "description": "Free: 3 analyses/day. Pro: $9/mo, unlimited/day. Business: $29/mo, 100/day. Team: $79/mo, 500/day."
40
40
  },
41
41
  "featureList": [
42
42
  "Headline analyzer with framework-based scoring",
@@ -1231,10 +1231,10 @@
1231
1231
  <div class="plan-badge">Most popular</div>
1232
1232
  <div class="plan-name">PRO</div>
1233
1233
  <div class="plan-price">$9<span>/mo</span></div>
1234
- <div class="plan-period">20 analyses/day · billed monthly</div>
1234
+ <div class="plan-period">Unlimited analyses/day · billed monthly</div>
1235
1235
  <a href="https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a" class="plan-cta" target="_blank" rel="noopener">Get Pro →</a>
1236
1236
  <ul class="plan-features">
1237
- <li><span class="check">✓</span> <strong>20 analyses/day</strong> — the free tier</li>
1237
+ <li><span class="check">✓</span> <strong>Unlimited analyses/day</strong> — no daily cap</li>
1238
1238
  <li><span class="check">✓</span> All 6 tools (CLI + dashboard)</li>
1239
1239
  <li><span class="check">✓</span> License key for CLI activation</li>
1240
1240
  <li><span class="check">✓</span> Batch mode — analyze whole directories</li>
@@ -1,4 +1,4 @@
1
- import { getStripe, isStripeConfigured, isAudienceDecoderConfigured, hasActiveSubscription, hasPurchased, getCustomerStripeId, upsertCustomer, saveSubscription, updateSubscriptionStatus, updateSubscriptionPeriod, saveOneTimePurchase, priceToPlanTier, } from '../services/stripe.js';
1
+ import { getStripe, isStripeConfigured, isAudienceDecoderConfigured, hasActiveSubscription, hasPurchased, getCustomerStripeId, upsertCustomer, saveSubscription, updateSubscriptionStatus, updateSubscriptionPeriod, saveOneTimePurchase, priceToPlanTier, hasActiveSubscriptionLive, } from '../services/stripe.js';
2
2
  import { upsertLicenseKey, getLicenseKeysForEmail, validateLicenseKey } from '../services/license.js';
3
3
  function successHtml(title, body) {
4
4
  return `<!DOCTYPE html>
@@ -205,7 +205,8 @@ export function registerStripeRoutes(app) {
205
205
  reply.status(400);
206
206
  return { error: 'Valid email required.' };
207
207
  }
208
- if (!hasActiveSubscription(email)) {
208
+ const isActive = await hasActiveSubscriptionLive(email);
209
+ if (!isActive) {
209
210
  reply.status(403);
210
211
  return { error: 'No active Content-Grade Pro subscription found for this email.' };
211
212
  }
@@ -277,9 +278,9 @@ export function registerStripeRoutes(app) {
277
278
  });
278
279
  // Upgrade redirect — CLI rate-limit messages point here for a clean, stable URL
279
280
  app.get('/upgrade', async (_req, reply) => {
280
- const teamLink = process.env.STRIPE_PAYMENT_LINK_CONTENTGRADE_TEAM
281
- ?? 'https://buy.stripe.com/cNiaEZfA8cu9bDv4088k80c';
282
- return reply.redirect(teamLink, 302);
281
+ const proLink = process.env.STRIPE_PAYMENT_LINK_CONTENTGRADE_PRO
282
+ ?? 'https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a'; // Pro $9/mo direct checkout
283
+ return reply.redirect(proLink, 302);
283
284
  });
284
285
  app.get('/api/stripe/subscription-status', async (req, reply) => {
285
286
  const query = req.query;
@@ -110,12 +110,25 @@ export async function getActiveSubscriptionLive(email) {
110
110
  const tier = active ? getSubscriptionTier(email) : 'free';
111
111
  return { isPro: active, tier };
112
112
  }
113
- const customerId = getCustomerStripeId(email);
113
+ let customerId = getCustomerStripeId(email);
114
+ // If customer not in local DB (common after Render cold start wipes /tmp/contentgrade.db),
115
+ // look up directly in Stripe by email and repopulate the local DB.
114
116
  if (!customerId) {
115
- const active = hasActiveSubscription(email);
116
- const tier = active ? getSubscriptionTier(email) : 'free';
117
- _subCache.set(email, { isPro: active, tier, expiresAt: Date.now() + SUB_CACHE_TTL_MS });
118
- return { isPro: active, tier };
117
+ try {
118
+ const customers = await stripe.customers.list({ email, limit: 1 });
119
+ if (customers.data.length > 0) {
120
+ customerId = customers.data[0].id;
121
+ upsertCustomer(email, customerId);
122
+ console.log(`[stripe] Repopulated customer from Stripe for ${email}`);
123
+ }
124
+ }
125
+ catch (lookupErr) {
126
+ console.error('[stripe] Customer email lookup failed:', lookupErr);
127
+ }
128
+ }
129
+ if (!customerId) {
130
+ _subCache.set(email, { isPro: false, tier: 'free', expiresAt: Date.now() + SUB_CACHE_ERROR_TTL_MS });
131
+ return { isPro: false, tier: 'free' };
119
132
  }
120
133
  try {
121
134
  const subscriptions = await stripe.subscriptions.list({
@@ -124,7 +137,22 @@ export async function getActiveSubscriptionLive(email) {
124
137
  limit: 1,
125
138
  });
126
139
  const isPro = subscriptions.data.length > 0;
127
- const tier = isPro ? getSubscriptionTier(email) : 'free';
140
+ let tier = 'free';
141
+ if (isPro) {
142
+ const priceId = subscriptions.data[0]?.items?.data[0]?.price?.id ?? '';
143
+ tier = priceToPlanTier(priceId);
144
+ // Repopulate subscription record if missing (DB wipe recovery)
145
+ if (!hasActiveSubscription(email)) {
146
+ saveSubscription({
147
+ email,
148
+ stripe_customer_id: customerId,
149
+ stripe_subscription_id: subscriptions.data[0].id,
150
+ plan_tier: tier,
151
+ status: 'active',
152
+ current_period_end: subscriptions.data[0].current_period_end,
153
+ });
154
+ }
155
+ }
128
156
  _subCache.set(email, { isPro, tier, expiresAt: Date.now() + SUB_CACHE_TTL_MS });
129
157
  return { isPro, tier };
130
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-grade",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
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": {