content-grade 1.0.23 → 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.
package/bin/content-grade.js
CHANGED
|
@@ -128,45 +128,57 @@ function showFreeTierCTA(count) {
|
|
|
128
128
|
hr();
|
|
129
129
|
|
|
130
130
|
if (remaining === 0) {
|
|
131
|
-
//
|
|
132
|
-
console.log(` ${RD}${B}
|
|
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(` ${
|
|
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(` ${
|
|
137
|
-
console.log(` ${
|
|
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 —
|
|
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}:`);
|
|
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(` ${
|
|
144
|
+
console.log(` ${MG}→ Pro adds CI mode + bulk grading. $9/mo: ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
156
145
|
} 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}`);
|
|
146
|
+
// Runs remaining — compact 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
|
|
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
|
|
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
|
|
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}
|
|
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(` ${
|
|
209
|
-
console.log(` ${
|
|
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(` ${
|
|
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
|
-
|
|
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}
|
|
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
|
|
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(` ${
|
|
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 ||
|
|
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
|
}
|
|
@@ -1042,21 +1050,13 @@ async function cmdBatch(dirPath) {
|
|
|
1042
1050
|
blank();
|
|
1043
1051
|
console.log(` ${B}${MG}Batch Analysis — Pro Feature${R}`);
|
|
1044
1052
|
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:`);
|
|
1053
|
+
console.log(` ${D}Batch mode grades every file in a directory in one shot — Pro only.${R}`);
|
|
1051
1054
|
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`);
|
|
1055
|
+
console.log(` Pro is $9/mo and also unlocks unlimited daily runs.`);
|
|
1055
1056
|
blank();
|
|
1056
|
-
console.log(` ${MG}${B}→
|
|
1057
|
-
console.log(` ${CY} ${UPGRADE_LINKS.free}${R}`);
|
|
1057
|
+
console.log(` ${MG}${B}→ Get Pro: ${CY}${UPGRADE_LINKS.free}${R}`);
|
|
1058
1058
|
blank();
|
|
1059
|
-
console.log(` ${D}Already have a key? ${CY}content-grade activate
|
|
1059
|
+
console.log(` ${D}Already have a key? ${CY}content-grade activate <key>${R}`);
|
|
1060
1060
|
blank();
|
|
1061
1061
|
process.exit(1);
|
|
1062
1062
|
}
|
|
@@ -1264,7 +1264,7 @@ function cmdStart() {
|
|
|
1264
1264
|
info(` EmailForge — ${url}/email-forge`);
|
|
1265
1265
|
info(` AudienceDecoder — ${url}/audience`);
|
|
1266
1266
|
blank();
|
|
1267
|
-
info(`Free tier:
|
|
1267
|
+
info(`Free tier: 3 analyses/day. Unlimited with Pro ($9/mo) → ${UPGRADE_LINKS.free}`);
|
|
1268
1268
|
info(`Press Ctrl+C to stop`);
|
|
1269
1269
|
blank();
|
|
1270
1270
|
openBrowser(url);
|
|
@@ -1439,7 +1439,7 @@ async function cmdMetrics() {
|
|
|
1439
1439
|
function cmdHelp() {
|
|
1440
1440
|
banner();
|
|
1441
1441
|
console.log(` ${D}AI-powered content quality scoring — readability, SEO, structure analysis${R}`);
|
|
1442
|
-
console.log(` ${D}v${_version} · ${CY}content-grade
|
|
1442
|
+
console.log(` ${D}v${_version} · ${CY}npmjs.com/package/content-grade${R}`);
|
|
1443
1443
|
blank();
|
|
1444
1444
|
console.log(` ${B}QUICK START${R}`);
|
|
1445
1445
|
blank();
|
|
@@ -1540,8 +1540,8 @@ function cmdHelp() {
|
|
|
1540
1540
|
console.log(` Business 100/day $29/mo`);
|
|
1541
1541
|
console.log(` Team 500/day $79/mo`);
|
|
1542
1542
|
blank();
|
|
1543
|
-
console.log(`
|
|
1544
|
-
console.log(`
|
|
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}`);
|
|
1545
1545
|
blank();
|
|
1546
1546
|
}
|
|
1547
1547
|
|
|
@@ -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