content-grade 1.0.23 → 1.0.25

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,58 @@ 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
+ // Last free run — strong CTA, no escape hatch
132
+ console.log(` ${RD}${B}${count}/${limit} free analyses used — daily limit reached.${R}`);
133
133
  blank();
134
- console.log(` ${B}Upgrade to Pro $9/mo${R} to keep going:`);
134
+ console.log(` ${WH}Pro removes the cap. Analyze your entire content backlog,${R}`);
135
+ console.log(` ${WH}run it in CI on every draft, batch-score a whole directory.${R}`);
135
136
  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`);
137
+ console.log(` ${D}Used by 1,100+ developers. $9/mo, cancel anytime.${R}`);
140
138
  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}`);
139
+ console.log(` ${MG}${B}→ Get Pro: ${CY}${UPGRADE_LINKS.free}${R}`);
140
+ console.log(` ${D} Have a key? ${CY}content-grade activate <key>${R}`);
145
141
  } 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}:`);
142
+ // 1 run left — tease Pro features
143
+ console.log(` ${YL}${B}${count}/${limit} free analyses used 1 left.${R}`);
150
144
  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}`);
154
- blank();
155
- console.log(` ${D}Or use your last run: ${CY}content-grade analyze ./another-post.md${R}`);
145
+ console.log(` ${MG} Pro: unlimited analyses + CI mode + bulk grading. $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
156
146
  } 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}`);
147
+ // Runs remaining — light nudge with value hook
148
+ console.log(` ${D}${count}/${limit} free analyses · ${remaining} left · Unlimited with Pro ($9/mo): ${CY}${UPGRADE_LINKS.free}${R}`);
169
149
  }
150
+ hr();
151
+ }
152
+
153
+ // ── Early Adopter program enrollment CTA ─────────────────────────────────────
154
+
155
+ const EARLY_ADOPTER_URL = 'https://github.com/StanislavBG/Content-Grade/discussions/new?category=show-and-tell';
156
+
157
+ function hasShownEarlyAdopterCTA() {
158
+ return Boolean(loadConfig().earlyAdopterCTAShown);
159
+ }
160
+
161
+ function markEarlyAdopterCTAShown() {
162
+ const cfg = loadConfig();
163
+ cfg.earlyAdopterCTAShown = true;
164
+ saveConfig(cfg);
165
+ }
166
+
167
+ // Show once, after the first successful run, to non-Pro users who haven't seen it.
168
+ function maybeShowEarlyAdopterCTA(count) {
169
+ if (isProUser()) return;
170
+ if (count !== 1) return; // only after the very first run today
171
+ if (hasShownEarlyAdopterCTA()) return; // only once per install
172
+ markEarlyAdopterCTAShown();
173
+ blank();
174
+ hr();
175
+ console.log(` ${GN}${B}Early Adopter program — 50 founding seats, limited spots remain.${R}`);
176
+ blank();
177
+ console.log(` ${WH}You just ran ContentGrade. That qualifies you.${R}`);
178
+ console.log(` ${D}Post in Show & Tell with ${CY}[Early Adopter]${R}${D} in the title → get 12 months Pro free.${R}`);
179
+ blank();
180
+ console.log(` ${D}Claim your seat: ${CY}${EARLY_ADOPTER_URL}${R}`);
181
+ console.log(` ${D}What's included: ${CY}https://github.com/StanislavBG/Content-Grade/blob/main/EARLY_ADOPTERS.md${R}`);
182
+ hr();
170
183
  }
171
184
 
172
185
  // Single-line usage counter appended after every command run.
@@ -177,12 +190,13 @@ function showUsageFooter(count) {
177
190
  const remaining = Math.max(0, limit - count);
178
191
  blank();
179
192
  if (remaining === 0) {
180
- console.log(` ${RD}[ ${count}/${limit} free runs used today · limit reached upgrade for unlimited: ${UPGRADE_LINKS.free} ]${R}`);
193
+ console.log(` ${RD}[ ${count}/${limit} free runs used limit reached. Unlimited runs: ${UPGRADE_LINKS.free} ]${R}`);
181
194
  } else if (remaining === 1) {
182
- console.log(` ${YL}[ ${count}/${limit} free runs used today · 1 remaining upgrade for unlimited: ${UPGRADE_LINKS.free} ]${R}`);
195
+ console.log(` ${YL}[ ${count}/${limit} free runs used · 1 left. Unlimited runs: ${UPGRADE_LINKS.free} ]${R}`);
183
196
  } else {
184
- console.log(` ${D}[ ${count}/${limit} free runs used today upgrade for unlimited: ${UPGRADE_LINKS.free} ]${R}`);
197
+ console.log(` ${D}[ ${count}/${limit} free runs today · ${remaining} left. Unlimited: ${UPGRADE_LINKS.free} ]${R}`);
185
198
  }
199
+ maybeShowEarlyAdopterCTA(count);
186
200
  }
187
201
 
188
202
  // Block a run before it starts if the free tier is exhausted.
@@ -196,19 +210,18 @@ function checkFreeTierLimit() {
196
210
 
197
211
  blank();
198
212
  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:`);
213
+ console.log(` ${YL}${B}${limit}/${limit} free analyses used today.${R}`);
202
214
  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`);
215
+ console.log(` ${D}Pro removes the cap — unlimited analyses, $9/mo:${R}`);
216
+ console.log(` ${D}├── Unlimited analyses — no cap, ever${R}`);
217
+ console.log(` ${D}├── Batch mode — score an entire /posts/ directory${R}`);
218
+ console.log(` ${D}├── CI mode — exit 1 on below-threshold content${R}`);
219
+ console.log(` ${D}└── JSON + HTML output — pipe into your pipeline${R}`);
207
220
  blank();
208
- console.log(` ${MG}${B}→ Upgrade to Pro — $9/mo${R}`);
209
- console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
221
+ console.log(` ${D}Used by 1,100+ developers.${R}`);
210
222
  blank();
211
- console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
223
+ console.log(` ${MG}${B}→ ${CY}${UPGRADE_LINKS.free}${R}`);
224
+ console.log(` ${D} Already have a key? ${CY}content-grade activate <key>${R}`);
212
225
  hr();
213
226
  blank();
214
227
  return true;
@@ -646,7 +659,7 @@ async function cmdAnalyze(filePath) {
646
659
  hr();
647
660
  console.log(` ${D}Pro active · Next: ${CY}content-grade batch ./posts/${R}${D} to analyze a whole directory${R}`);
648
661
  } else {
649
- showUsageFooter(usageCount);
662
+ showFreeTierCTA(usageCount);
650
663
  }
651
664
 
652
665
  // CI exit code — shown after full output so user sees the score before exit
@@ -887,7 +900,7 @@ async function cmdInit() {
887
900
  console.log(` ${CY}content-grade start${R}`);
888
901
  blank();
889
902
  }
890
- console.log(` ${D}Pro tier: unlimited analyses/day + batch mode ${CY}content-grade.onrender.com${R}`);
903
+ console.log(` ${D}Need unlimited runs? Pro is $9/mo ${CY}${UPGRADE_LINKS.free}${R}`);
891
904
  blank();
892
905
  }
893
906
 
@@ -912,11 +925,9 @@ async function cmdActivate() {
912
925
  return;
913
926
  }
914
927
 
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}`);
928
+ console.log(` ${D}Pro unlocks unlimited runs/day + batch mode for $9/mo:${R}`);
918
929
  blank();
919
- console.log(` ${D}Get a license: ${CY}content-grade.onrender.com${R}${D} → Pricing${R}`);
930
+ console.log(` ${MG}${B}→ Get Pro: ${CY}${UPGRADE_LINKS.free}${R}`);
920
931
  blank();
921
932
 
922
933
  let key;
@@ -997,7 +1008,7 @@ async function cmdActivate() {
997
1008
  } else {
998
1009
  process.stdout.write('\n');
999
1010
  blank();
1000
- fail(data.message || 'Invalid or expired license key. Visit content-grade.onrender.com to check your subscription.');
1011
+ fail(data.message || `Invalid or expired license key. Get a new key: ${UPGRADE_LINKS.free}`);
1001
1012
  blank();
1002
1013
  process.exit(1);
1003
1014
  }
@@ -1042,21 +1053,13 @@ async function cmdBatch(dirPath) {
1042
1053
  blank();
1043
1054
  console.log(` ${B}${MG}Batch Analysis — Pro Feature${R}`);
1044
1055
  blank();
1045
- console.log(` ${D}Batch mode requires a Pro license.${R}`);
1046
- blank();
1047
- console.log(` ${D}Free tier: analyze files one at a time.${R}`);
1048
- console.log(` ${CY}content-grade analyze ./post.md${R}`);
1049
- blank();
1050
- console.log(` ${B}Upgrade to Pro — $9/mo${R} to unlock batch mode:`);
1056
+ console.log(` ${D}Batch mode grades every file in a directory in one shot — Pro only.${R}`);
1051
1057
  blank();
1052
- console.log(` ${GN}✓${R} ${B}Unlimited analyses${R} no daily cap`);
1053
- console.log(` ${GN}✓${R} ${B}Batch mode${R} grade an entire directory at once`);
1054
- console.log(` ${GN}✓${R} ${B}Priority support${R} direct email response within 24h`);
1058
+ console.log(` Pro is $9/mo and also unlocks unlimited daily runs.`);
1055
1059
  blank();
1056
- console.log(` ${MG}${B}→ Upgrade to Pro $9/mo${R}`);
1057
- console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
1060
+ console.log(` ${MG}${B}→ Get Pro: ${CY}${UPGRADE_LINKS.free}${R}`);
1058
1061
  blank();
1059
- console.log(` ${D}Already have a key? ${CY}content-grade activate${R}`);
1062
+ console.log(` ${D}Already have a key? ${CY}content-grade activate <key>${R}`);
1060
1063
  blank();
1061
1064
  process.exit(1);
1062
1065
  }
@@ -1264,7 +1267,7 @@ function cmdStart() {
1264
1267
  info(` EmailForge — ${url}/email-forge`);
1265
1268
  info(` AudienceDecoder — ${url}/audience`);
1266
1269
  blank();
1267
- info(`Free tier: 5 analyses/day. Upgrade at ${url}`);
1270
+ info(`Free tier: 3 analyses/day. Unlimited with Pro ($9/mo) → ${UPGRADE_LINKS.free}`);
1268
1271
  info(`Press Ctrl+C to stop`);
1269
1272
  blank();
1270
1273
  openBrowser(url);
@@ -1439,7 +1442,7 @@ async function cmdMetrics() {
1439
1442
  function cmdHelp() {
1440
1443
  banner();
1441
1444
  console.log(` ${D}AI-powered content quality scoring — readability, SEO, structure analysis${R}`);
1442
- console.log(` ${D}v${_version} · ${CY}content-grade.onrender.com${R}`);
1445
+ console.log(` ${D}v${_version} · ${CY}npmjs.com/package/content-grade${R}`);
1443
1446
  blank();
1444
1447
  console.log(` ${B}QUICK START${R}`);
1445
1448
  blank();
@@ -1540,8 +1543,8 @@ function cmdHelp() {
1540
1543
  console.log(` Business 100/day $29/mo`);
1541
1544
  console.log(` Team 500/day $79/mo`);
1542
1545
  blank();
1543
- console.log(` Purchase at: ${CY}content-grade.onrender.com${R}`);
1544
- console.log(` Then unlock: ${CY}content-grade activate <your-license-key>${R}`);
1546
+ console.log(` Get Pro: ${CY}${UPGRADE_LINKS.free}${R} ${D}# direct checkout${R}`);
1547
+ console.log(` Activate: ${CY}content-grade activate <your-license-key>${R}`);
1545
1548
  blank();
1546
1549
  }
1547
1550
 
@@ -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.23",
3
+ "version": "1.0.25",
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": {