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.
package/bin/content-grade.js
CHANGED
|
@@ -128,45 +128,58 @@ function showFreeTierCTA(count) {
|
|
|
128
128
|
hr();
|
|
129
129
|
|
|
130
130
|
if (remaining === 0) {
|
|
131
|
-
// Last free run
|
|
132
|
-
console.log(` ${RD}${B}
|
|
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(` ${
|
|
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(` ${
|
|
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}→
|
|
142
|
-
console.log(` ${
|
|
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 —
|
|
147
|
-
console.log(` ${YL}${B}${count}/${limit} free
|
|
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(` ${
|
|
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
|
-
//
|
|
158
|
-
console.log(` ${D}
|
|
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
|
|
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
|
|
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
|
|
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(` ${
|
|
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(` ${
|
|
204
|
-
console.log(` ${
|
|
205
|
-
console.log(` ${
|
|
206
|
-
console.log(` ${
|
|
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(` ${
|
|
209
|
-
console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
|
|
221
|
+
console.log(` ${D}Used by 1,100+ developers.${R}`);
|
|
210
222
|
blank();
|
|
211
|
-
console.log(` ${
|
|
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
|
-
|
|
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}
|
|
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
|
|
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(` ${
|
|
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 ||
|
|
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
|
|
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(`
|
|
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}→
|
|
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
|
|
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:
|
|
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
|
|
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(`
|
|
1544
|
-
console.log(`
|
|
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
|
-
|
|
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
|
|
281
|
-
?? 'https://buy.stripe.com/
|
|
282
|
-
return reply.redirect(
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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