content-grade 1.0.19 → 1.0.21
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/CONTRIBUTING.md +61 -11
- package/README.md +14 -0
- package/bin/content-grade.js +124 -46
- package/bin/telemetry.js +4 -4
- package/dist/landing.html +1477 -0
- package/dist/privacy.html +189 -0
- package/dist/terms.html +185 -0
- package/dist-server/server/db.js +6 -0
- package/dist-server/server/index.js +2 -0
- package/dist-server/server/routes/analytics.js +3 -3
- package/dist-server/server/routes/demos.js +10 -8
- package/dist-server/server/routes/stripe.js +82 -3
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Privacy Policy — ContentGrade</title>
|
|
7
|
+
<meta name="description" content="Privacy Policy for ContentGrade — the local AI content grader suite." />
|
|
8
|
+
<link rel="canonical" href="https://content-grade.github.io/Content-Grade/privacy.html" />
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0a0a0f;
|
|
12
|
+
--bg2: #0f0f1a;
|
|
13
|
+
--bg3: #141424;
|
|
14
|
+
--border: rgba(255,255,255,0.08);
|
|
15
|
+
--text: #e0e0e0;
|
|
16
|
+
--muted: #888;
|
|
17
|
+
--accent: #7c4dff;
|
|
18
|
+
--accent-light: #9e7aff;
|
|
19
|
+
}
|
|
20
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21
|
+
html { scroll-behavior: smooth; }
|
|
22
|
+
body {
|
|
23
|
+
background: var(--bg);
|
|
24
|
+
color: var(--text);
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
|
|
26
|
+
font-size: 16px;
|
|
27
|
+
line-height: 1.7;
|
|
28
|
+
}
|
|
29
|
+
a { color: var(--accent-light); text-decoration: none; }
|
|
30
|
+
a:hover { text-decoration: underline; }
|
|
31
|
+
nav {
|
|
32
|
+
position: sticky;
|
|
33
|
+
top: 0;
|
|
34
|
+
background: rgba(10,10,15,0.92);
|
|
35
|
+
border-bottom: 1px solid var(--border);
|
|
36
|
+
padding: 14px 24px;
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 16px;
|
|
40
|
+
backdrop-filter: blur(8px);
|
|
41
|
+
z-index: 100;
|
|
42
|
+
}
|
|
43
|
+
.nav-logo {
|
|
44
|
+
font-size: 15px;
|
|
45
|
+
font-weight: 700;
|
|
46
|
+
color: var(--text);
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 8px;
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
}
|
|
52
|
+
.nav-logo:hover { text-decoration: none; color: var(--accent-light); }
|
|
53
|
+
.logo-icon { font-size: 18px; }
|
|
54
|
+
.nav-back { font-size: 13px; color: var(--muted); margin-left: auto; }
|
|
55
|
+
.nav-back:hover { color: var(--text); }
|
|
56
|
+
main {
|
|
57
|
+
max-width: 720px;
|
|
58
|
+
margin: 0 auto;
|
|
59
|
+
padding: 60px 24px 80px;
|
|
60
|
+
}
|
|
61
|
+
.page-header { margin-bottom: 48px; }
|
|
62
|
+
.page-header h1 {
|
|
63
|
+
font-size: 2rem;
|
|
64
|
+
font-weight: 700;
|
|
65
|
+
margin-bottom: 8px;
|
|
66
|
+
}
|
|
67
|
+
.page-header p { color: var(--muted); font-size: 14px; }
|
|
68
|
+
h2 {
|
|
69
|
+
font-size: 1.1rem;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
color: var(--text);
|
|
72
|
+
margin: 36px 0 12px;
|
|
73
|
+
padding-bottom: 8px;
|
|
74
|
+
border-bottom: 1px solid var(--border);
|
|
75
|
+
}
|
|
76
|
+
p { margin-bottom: 16px; }
|
|
77
|
+
ul { margin: 12px 0 16px 20px; }
|
|
78
|
+
li { margin-bottom: 6px; }
|
|
79
|
+
.highlight {
|
|
80
|
+
background: var(--bg3);
|
|
81
|
+
border: 1px solid var(--border);
|
|
82
|
+
border-left: 3px solid var(--accent);
|
|
83
|
+
border-radius: 6px;
|
|
84
|
+
padding: 16px 20px;
|
|
85
|
+
margin: 24px 0;
|
|
86
|
+
font-size: 15px;
|
|
87
|
+
}
|
|
88
|
+
footer {
|
|
89
|
+
border-top: 1px solid var(--border);
|
|
90
|
+
padding: 32px 24px;
|
|
91
|
+
text-align: center;
|
|
92
|
+
color: var(--muted);
|
|
93
|
+
font-size: 13px;
|
|
94
|
+
}
|
|
95
|
+
footer a { color: var(--muted); }
|
|
96
|
+
footer a:hover { color: var(--accent-light); text-decoration: none; }
|
|
97
|
+
.footer-links { display: flex; justify-content: center; gap: 24px; margin-top: 12px; }
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
|
|
102
|
+
<nav>
|
|
103
|
+
<a class="nav-logo" href="./landing.html">
|
|
104
|
+
<span class="logo-icon">✦</span>
|
|
105
|
+
ContentGrade
|
|
106
|
+
</a>
|
|
107
|
+
<a class="nav-back" href="./landing.html">← Back to home</a>
|
|
108
|
+
</nav>
|
|
109
|
+
|
|
110
|
+
<main>
|
|
111
|
+
<div class="page-header">
|
|
112
|
+
<h1>Privacy Policy</h1>
|
|
113
|
+
<p>Last updated: March 2026</p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="highlight">
|
|
117
|
+
<strong>Short version:</strong> ContentGrade runs entirely on your local machine. Your content never leaves your device. Optional, opt-in telemetry collects only aggregate usage statistics — no personal data, no content, no identifiers.
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<h2>1. What ContentGrade Is</h2>
|
|
121
|
+
<p>ContentGrade is an open-source CLI tool for AI-powered content quality analysis. It runs locally on your computer via the Claude CLI. All analysis is performed on-device — ContentGrade has no servers, no cloud backend, and no data pipeline.</p>
|
|
122
|
+
|
|
123
|
+
<h2>2. Data We Collect</h2>
|
|
124
|
+
<p>ContentGrade collects <strong>no data by default</strong>. There is an optional, opt-in telemetry system that you must explicitly enable by running:</p>
|
|
125
|
+
<p style="background:var(--bg3);padding:10px 14px;border-radius:6px;font-family:monospace;font-size:14px;">content-grade telemetry on</p>
|
|
126
|
+
<p>If you enable telemetry, the following <strong>anonymous, aggregate</strong> statistics may be sent:</p>
|
|
127
|
+
<ul>
|
|
128
|
+
<li>Which command was used (e.g., "grade", "headline", "batch")</li>
|
|
129
|
+
<li>Whether the command succeeded or failed (no error messages, no content)</li>
|
|
130
|
+
<li>Approximate duration of the command</li>
|
|
131
|
+
<li>Score range buckets (e.g., "60–70") — not exact scores, never your content</li>
|
|
132
|
+
<li>A random session ID that resets every 24 hours (not linked to you)</li>
|
|
133
|
+
</ul>
|
|
134
|
+
<p>We <strong>never</strong> collect: your content, your API keys, your file paths, your IP address, or any personally identifiable information.</p>
|
|
135
|
+
<p>You can disable telemetry at any time: <code>content-grade telemetry off</code></p>
|
|
136
|
+
|
|
137
|
+
<h2>3. Landing Page Analytics</h2>
|
|
138
|
+
<p>The ContentGrade website uses <a href="https://www.goatcounter.com" target="_blank" rel="noopener">GoatCounter</a> for privacy-friendly page view analytics. GoatCounter:</p>
|
|
139
|
+
<ul>
|
|
140
|
+
<li>Does not use cookies</li>
|
|
141
|
+
<li>Does not collect personal data or IP addresses</li>
|
|
142
|
+
<li>Does not track users across sites</li>
|
|
143
|
+
<li>Is open source and GDPR-compliant by design</li>
|
|
144
|
+
</ul>
|
|
145
|
+
|
|
146
|
+
<h2>4. Third-Party Services</h2>
|
|
147
|
+
<p>ContentGrade depends on the following third-party services, each with their own privacy practices:</p>
|
|
148
|
+
<ul>
|
|
149
|
+
<li><strong>Anthropic Claude CLI</strong> — ContentGrade uses Claude CLI to perform content analysis. Your content is sent to Anthropic's API as part of normal Claude CLI operation. Review <a href="https://www.anthropic.com/privacy" target="_blank" rel="noopener">Anthropic's Privacy Policy</a> for details on how they handle data.</li>
|
|
150
|
+
<li><strong>npm Registry</strong> — Used only for installation. No ongoing data collection.</li>
|
|
151
|
+
</ul>
|
|
152
|
+
<p>ContentGrade does not sell or share your data with any third parties.</p>
|
|
153
|
+
|
|
154
|
+
<h2>5. Your Rights (GDPR)</h2>
|
|
155
|
+
<p>If you are in the European Union or UK, you have the following rights under GDPR:</p>
|
|
156
|
+
<ul>
|
|
157
|
+
<li><strong>Right of access</strong> — You can request a copy of any data we hold about you.</li>
|
|
158
|
+
<li><strong>Right to erasure</strong> — You can request deletion of any data associated with you.</li>
|
|
159
|
+
<li><strong>Right to object</strong> — You can opt out of telemetry at any time with <code>content-grade telemetry off</code>.</li>
|
|
160
|
+
</ul>
|
|
161
|
+
<p>Because ContentGrade does not collect personal data by default, GDPR obligations are minimal. The opt-in telemetry stores no personal identifiers. If you have a concern, contact us and we will respond within 30 days.</p>
|
|
162
|
+
|
|
163
|
+
<h2>6. Data Retention</h2>
|
|
164
|
+
<p>Telemetry data (if enabled) is aggregate-only and retained for up to 12 months for product improvement. No content or personal data is ever stored.</p>
|
|
165
|
+
|
|
166
|
+
<h2>7. Children's Privacy</h2>
|
|
167
|
+
<p>ContentGrade is a developer tool not directed at children under 13. We do not knowingly collect information from children.</p>
|
|
168
|
+
|
|
169
|
+
<h2>8. Changes to This Policy</h2>
|
|
170
|
+
<p>We may update this Privacy Policy from time to time. The "Last updated" date at the top reflects the most recent revision. Continued use of ContentGrade after changes constitutes acceptance of the updated policy.</p>
|
|
171
|
+
|
|
172
|
+
<h2>9. Contact</h2>
|
|
173
|
+
<p>Questions or requests about this Privacy Policy:</p>
|
|
174
|
+
<p><a href="mailto:bilko@bglabs.app">bilko@bglabs.app</a></p>
|
|
175
|
+
<p>ContentGrade is open source: <a href="https://github.com/Content-Grade/Content-Grade" target="_blank" rel="noopener">github.com/Content-Grade/Content-Grade</a></p>
|
|
176
|
+
</main>
|
|
177
|
+
|
|
178
|
+
<footer>
|
|
179
|
+
<span>© 2026 ContentGrade. MIT Licensed.</span>
|
|
180
|
+
<div class="footer-links">
|
|
181
|
+
<a href="./landing.html">Home</a>
|
|
182
|
+
<a href="./privacy.html">Privacy Policy</a>
|
|
183
|
+
<a href="./terms.html">Terms of Service</a>
|
|
184
|
+
<a href="mailto:bilko@bglabs.app">Contact</a>
|
|
185
|
+
</div>
|
|
186
|
+
</footer>
|
|
187
|
+
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
package/dist/terms.html
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Terms of Service — ContentGrade</title>
|
|
7
|
+
<meta name="description" content="Terms of Service for ContentGrade — the local AI content grader suite." />
|
|
8
|
+
<link rel="canonical" href="https://content-grade.github.io/Content-Grade/terms.html" />
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0a0a0f;
|
|
12
|
+
--bg2: #0f0f1a;
|
|
13
|
+
--bg3: #141424;
|
|
14
|
+
--border: rgba(255,255,255,0.08);
|
|
15
|
+
--text: #e0e0e0;
|
|
16
|
+
--muted: #888;
|
|
17
|
+
--accent: #7c4dff;
|
|
18
|
+
--accent-light: #9e7aff;
|
|
19
|
+
}
|
|
20
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21
|
+
html { scroll-behavior: smooth; }
|
|
22
|
+
body {
|
|
23
|
+
background: var(--bg);
|
|
24
|
+
color: var(--text);
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
|
|
26
|
+
font-size: 16px;
|
|
27
|
+
line-height: 1.7;
|
|
28
|
+
}
|
|
29
|
+
a { color: var(--accent-light); text-decoration: none; }
|
|
30
|
+
a:hover { text-decoration: underline; }
|
|
31
|
+
nav {
|
|
32
|
+
position: sticky;
|
|
33
|
+
top: 0;
|
|
34
|
+
background: rgba(10,10,15,0.92);
|
|
35
|
+
border-bottom: 1px solid var(--border);
|
|
36
|
+
padding: 14px 24px;
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 16px;
|
|
40
|
+
backdrop-filter: blur(8px);
|
|
41
|
+
z-index: 100;
|
|
42
|
+
}
|
|
43
|
+
.nav-logo {
|
|
44
|
+
font-size: 15px;
|
|
45
|
+
font-weight: 700;
|
|
46
|
+
color: var(--text);
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 8px;
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
}
|
|
52
|
+
.nav-logo:hover { text-decoration: none; color: var(--accent-light); }
|
|
53
|
+
.logo-icon { font-size: 18px; }
|
|
54
|
+
.nav-back { font-size: 13px; color: var(--muted); margin-left: auto; }
|
|
55
|
+
.nav-back:hover { color: var(--text); }
|
|
56
|
+
main {
|
|
57
|
+
max-width: 720px;
|
|
58
|
+
margin: 0 auto;
|
|
59
|
+
padding: 60px 24px 80px;
|
|
60
|
+
}
|
|
61
|
+
.page-header { margin-bottom: 48px; }
|
|
62
|
+
.page-header h1 {
|
|
63
|
+
font-size: 2rem;
|
|
64
|
+
font-weight: 700;
|
|
65
|
+
margin-bottom: 8px;
|
|
66
|
+
}
|
|
67
|
+
.page-header p { color: var(--muted); font-size: 14px; }
|
|
68
|
+
h2 {
|
|
69
|
+
font-size: 1.1rem;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
color: var(--text);
|
|
72
|
+
margin: 36px 0 12px;
|
|
73
|
+
padding-bottom: 8px;
|
|
74
|
+
border-bottom: 1px solid var(--border);
|
|
75
|
+
}
|
|
76
|
+
p { margin-bottom: 16px; }
|
|
77
|
+
ul { margin: 12px 0 16px 20px; }
|
|
78
|
+
li { margin-bottom: 6px; }
|
|
79
|
+
.highlight {
|
|
80
|
+
background: var(--bg3);
|
|
81
|
+
border: 1px solid var(--border);
|
|
82
|
+
border-left: 3px solid var(--accent);
|
|
83
|
+
border-radius: 6px;
|
|
84
|
+
padding: 16px 20px;
|
|
85
|
+
margin: 24px 0;
|
|
86
|
+
font-size: 15px;
|
|
87
|
+
}
|
|
88
|
+
footer {
|
|
89
|
+
border-top: 1px solid var(--border);
|
|
90
|
+
padding: 32px 24px;
|
|
91
|
+
text-align: center;
|
|
92
|
+
color: var(--muted);
|
|
93
|
+
font-size: 13px;
|
|
94
|
+
}
|
|
95
|
+
footer a { color: var(--muted); }
|
|
96
|
+
footer a:hover { color: var(--accent-light); text-decoration: none; }
|
|
97
|
+
.footer-links { display: flex; justify-content: center; gap: 24px; margin-top: 12px; }
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
|
|
102
|
+
<nav>
|
|
103
|
+
<a class="nav-logo" href="./landing.html">
|
|
104
|
+
<span class="logo-icon">✦</span>
|
|
105
|
+
ContentGrade
|
|
106
|
+
</a>
|
|
107
|
+
<a class="nav-back" href="./landing.html">← Back to home</a>
|
|
108
|
+
</nav>
|
|
109
|
+
|
|
110
|
+
<main>
|
|
111
|
+
<div class="page-header">
|
|
112
|
+
<h1>Terms of Service</h1>
|
|
113
|
+
<p>Last updated: March 2026</p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="highlight">
|
|
117
|
+
<strong>Short version:</strong> ContentGrade is open-source software (MIT license). Use it freely. We provide no warranties. You agree not to use it for spam or anything illegal.
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<h2>1. Acceptance of Terms</h2>
|
|
121
|
+
<p>By installing or using ContentGrade ("the Software"), you agree to these Terms of Service. If you do not agree, do not use the Software.</p>
|
|
122
|
+
|
|
123
|
+
<h2>2. License</h2>
|
|
124
|
+
<p>ContentGrade is released under the <strong>MIT License</strong>. You are free to:</p>
|
|
125
|
+
<ul>
|
|
126
|
+
<li>Use the Software for any purpose, commercial or personal</li>
|
|
127
|
+
<li>Modify, copy, and distribute the Software</li>
|
|
128
|
+
<li>Include the Software in your own projects</li>
|
|
129
|
+
</ul>
|
|
130
|
+
<p>The full license text is included in the <a href="https://github.com/Content-Grade/Content-Grade/blob/main/LICENSE" target="_blank" rel="noopener">GitHub repository</a>.</p>
|
|
131
|
+
|
|
132
|
+
<h2>3. Acceptable Use</h2>
|
|
133
|
+
<p>You agree not to use ContentGrade to:</p>
|
|
134
|
+
<ul>
|
|
135
|
+
<li>Generate or score content intended for spam, phishing, or fraud</li>
|
|
136
|
+
<li>Violate any applicable law or regulation</li>
|
|
137
|
+
<li>Circumvent safety measures in third-party services</li>
|
|
138
|
+
<li>Impersonate others or create misleading content at scale</li>
|
|
139
|
+
</ul>
|
|
140
|
+
|
|
141
|
+
<h2>4. Third-Party Services</h2>
|
|
142
|
+
<p>ContentGrade uses the Anthropic Claude CLI for content analysis. Your use of Claude CLI is subject to <a href="https://www.anthropic.com/legal/consumer-terms" target="_blank" rel="noopener">Anthropic's Terms of Service</a>. ContentGrade is not affiliated with or endorsed by Anthropic.</p>
|
|
143
|
+
|
|
144
|
+
<h2>5. No Warranties</h2>
|
|
145
|
+
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. ContentGrade's scores and analysis are AI-generated suggestions — not professional advice. Do not rely on them as the sole basis for business decisions.</p>
|
|
146
|
+
|
|
147
|
+
<h2>6. Limitation of Liability</h2>
|
|
148
|
+
<p>IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY — WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE — ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THE SOFTWARE.</p>
|
|
149
|
+
|
|
150
|
+
<h2>7. Pro Tier</h2>
|
|
151
|
+
<p>ContentGrade Pro is a paid tier providing additional features. Payment is processed by Stripe. Refunds are handled on a case-by-case basis — contact <a href="mailto:bilko@bglabs.app">bilko@bglabs.app</a> within 14 days of purchase if the Software does not function as described.</p>
|
|
152
|
+
|
|
153
|
+
<h2>8. Usage Limits and Abuse</h2>
|
|
154
|
+
<p>Free tier accounts are limited to 5 analyses per day. Paid plans have usage limits as described at the time of purchase. We reserve the right to suspend or terminate any account that exceeds its plan limits, engages in automated abuse, or violates these Terms, with or without notice.</p>
|
|
155
|
+
|
|
156
|
+
<h2>9. Termination</h2>
|
|
157
|
+
<p>We may terminate or suspend your access to the Service immediately, without prior notice, for conduct that we determine violates these Terms or is harmful to the Service, other users, or third parties.</p>
|
|
158
|
+
|
|
159
|
+
<h2>10. Indemnification</h2>
|
|
160
|
+
<p>You agree to indemnify, defend, and hold harmless ContentGrade and its operators from any claims, damages, losses, liabilities, costs, or expenses (including reasonable attorneys' fees) arising from your use of the Service, your violation of these Terms, or your violation of any third-party rights.</p>
|
|
161
|
+
|
|
162
|
+
<h2>11. Changes to Terms</h2>
|
|
163
|
+
<p>We may update these Terms from time to time. The "Last updated" date at the top reflects the most recent revision. Continued use after changes constitutes acceptance.</p>
|
|
164
|
+
|
|
165
|
+
<h2>12. Governing Law</h2>
|
|
166
|
+
<p>These Terms shall be governed by and construed in accordance with the laws of the State of Delaware, United States, without regard to conflict of law principles. Any legal action shall be brought exclusively in the courts of Delaware.</p>
|
|
167
|
+
|
|
168
|
+
<h2>13. Contact</h2>
|
|
169
|
+
<p>Questions about these Terms:</p>
|
|
170
|
+
<p><a href="mailto:bilko@bglabs.app">bilko@bglabs.app</a></p>
|
|
171
|
+
<p>ContentGrade is open source: <a href="https://github.com/Content-Grade/Content-Grade" target="_blank" rel="noopener">github.com/Content-Grade/Content-Grade</a></p>
|
|
172
|
+
</main>
|
|
173
|
+
|
|
174
|
+
<footer>
|
|
175
|
+
<span>© 2026 ContentGrade. MIT Licensed.</span>
|
|
176
|
+
<div class="footer-links">
|
|
177
|
+
<a href="./landing.html">Home</a>
|
|
178
|
+
<a href="./privacy.html">Privacy Policy</a>
|
|
179
|
+
<a href="./terms.html">Terms of Service</a>
|
|
180
|
+
<a href="mailto:bilko@bglabs.app">Contact</a>
|
|
181
|
+
</div>
|
|
182
|
+
</footer>
|
|
183
|
+
|
|
184
|
+
</body>
|
|
185
|
+
</html>
|
package/dist-server/server/db.js
CHANGED
|
@@ -93,6 +93,7 @@ function migrate(db) {
|
|
|
93
93
|
install_id TEXT NOT NULL,
|
|
94
94
|
event TEXT NOT NULL,
|
|
95
95
|
command TEXT,
|
|
96
|
+
is_pro INTEGER,
|
|
96
97
|
duration_ms INTEGER,
|
|
97
98
|
success INTEGER,
|
|
98
99
|
exit_code INTEGER,
|
|
@@ -124,4 +125,9 @@ function migrate(db) {
|
|
|
124
125
|
CREATE INDEX IF NOT EXISTS idx_license_keys_key ON license_keys(key);
|
|
125
126
|
CREATE INDEX IF NOT EXISTS idx_license_keys_email ON license_keys(email);
|
|
126
127
|
`);
|
|
128
|
+
// Additive migrations for existing DBs (safe to re-run — fails silently if column exists)
|
|
129
|
+
try {
|
|
130
|
+
db.exec(`ALTER TABLE cli_telemetry ADD COLUMN is_pro INTEGER`);
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
127
133
|
}
|
|
@@ -7,6 +7,7 @@ import { existsSync } from 'fs';
|
|
|
7
7
|
import { getDb } from './db.js';
|
|
8
8
|
import { registerDemoRoutes } from './routes/demos.js';
|
|
9
9
|
import { registerStripeRoutes } from './routes/stripe.js';
|
|
10
|
+
import { registerAnalyticsRoutes } from './routes/analytics.js';
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const PORT = parseInt(process.env.PORT || '4000', 10);
|
|
12
13
|
const isProd = process.env.NODE_ENV === 'production';
|
|
@@ -37,6 +38,7 @@ await app.register(cors, {
|
|
|
37
38
|
// Register API routes
|
|
38
39
|
registerDemoRoutes(app);
|
|
39
40
|
registerStripeRoutes(app);
|
|
41
|
+
registerAnalyticsRoutes(app);
|
|
40
42
|
// Health check
|
|
41
43
|
app.get('/api/health', async () => ({
|
|
42
44
|
status: 'alive',
|
|
@@ -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, duration_ms, success, exit_code, score, content_type, version, platform, node_version)
|
|
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.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);
|
|
46
|
+
(install_id, event, command, is_pro, duration_ms, success, exit_code, score, content_type, version, platform, node_version)
|
|
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);
|
|
49
49
|
}
|
|
50
50
|
catch {
|
|
51
51
|
// never fail — telemetry is non-critical
|
|
@@ -5,20 +5,22 @@ import { getDb } from '../db.js';
|
|
|
5
5
|
import { askClaude } from '../claude.js';
|
|
6
6
|
import { getActiveSubscriptionLive, hasPurchased } from '../services/stripe.js';
|
|
7
7
|
// ── Usage tracking utilities ──────────────────────────────────────
|
|
8
|
-
const FREE_TIER_LIMIT =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
8
|
+
const FREE_TIER_LIMIT = 3;
|
|
9
|
+
// Pro/Business/Team subscribers get effectively unlimited runs. 1_000_000 is large
|
|
10
|
+
// enough to never be hit in practice and serialises cleanly to JSON (unlike Infinity).
|
|
11
|
+
const PRO_TIER_LIMIT = 1_000_000;
|
|
12
|
+
const BUSINESS_TIER_LIMIT = 1_000_000;
|
|
13
|
+
const TEAM_TIER_LIMIT = 1_000_000;
|
|
12
14
|
const HEADLINE_GRADER_ENDPOINT = 'headline-grader';
|
|
13
|
-
const UPGRADE_URL = 'https://content-grade.
|
|
15
|
+
const UPGRADE_URL = 'https://content-grade.onrender.com/upgrade';
|
|
14
16
|
const TIER_LIMITS = {
|
|
15
17
|
free: FREE_TIER_LIMIT,
|
|
16
18
|
pro: PRO_TIER_LIMIT,
|
|
17
19
|
business: BUSINESS_TIER_LIMIT,
|
|
18
20
|
team: TEAM_TIER_LIMIT,
|
|
19
21
|
};
|
|
20
|
-
function freeGateMsg(
|
|
21
|
-
return `Free daily limit reached (${FREE_TIER_LIMIT}/day).
|
|
22
|
+
function freeGateMsg(_what) {
|
|
23
|
+
return `Free daily limit reached (${FREE_TIER_LIMIT}/day). Upgrade to Pro for unlimited: ${UPGRADE_URL}`;
|
|
22
24
|
}
|
|
23
25
|
function paidGateMsg(limit) {
|
|
24
26
|
return `Daily limit reached (${limit}/day). Upgrade for more at content-grade.onrender.com/#pricing. Resets at midnight UTC.`;
|
|
@@ -270,7 +272,7 @@ Respond ONLY with valid JSON matching this exact schema — no markdown, no extr
|
|
|
270
272
|
remaining: 0,
|
|
271
273
|
limit: hgcRate.limit,
|
|
272
274
|
message: hgcRate.isPro
|
|
273
|
-
? paidGateMsg(
|
|
275
|
+
? paidGateMsg(hgcRate.limit) : freeGateMsg('Upgrade for more at content-grade.onrender.com/#pricing'),
|
|
274
276
|
};
|
|
275
277
|
}
|
|
276
278
|
const scoringSystemPrompt = `You are a world-class direct response copywriting analyst. You evaluate headlines using four proven conversion frameworks.
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import { getStripe, isStripeConfigured, isAudienceDecoderConfigured, hasActiveSubscription, hasPurchased, getCustomerStripeId, upsertCustomer, saveSubscription, updateSubscriptionStatus, updateSubscriptionPeriod, saveOneTimePurchase, priceToPlanTier, } from '../services/stripe.js';
|
|
2
2
|
import { upsertLicenseKey, getLicenseKeysForEmail, validateLicenseKey } from '../services/license.js';
|
|
3
|
+
function successHtml(title, body) {
|
|
4
|
+
return `<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8"/>
|
|
8
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
9
|
+
<title>${title} — Content-Grade</title>
|
|
10
|
+
<style>
|
|
11
|
+
body{font-family:system-ui,sans-serif;background:#0d0d0d;color:#e8e8e8;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
|
|
12
|
+
.card{max-width:600px;width:90%;background:#1a1a1a;border:1px solid #333;border-radius:12px;padding:40px}
|
|
13
|
+
h1{margin-top:0;font-size:1.6em}
|
|
14
|
+
pre{overflow-x:auto;white-space:pre-wrap;word-break:break-all}
|
|
15
|
+
a{color:#7fc4ff}
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<div class="card">
|
|
20
|
+
<h1>${title}</h1>
|
|
21
|
+
${body}
|
|
22
|
+
<p><a href="https://content-grade.onrender.com">← Back to Content-Grade</a></p>
|
|
23
|
+
</div>
|
|
24
|
+
</body>
|
|
25
|
+
</html>`;
|
|
26
|
+
}
|
|
3
27
|
export function registerStripeRoutes(app) {
|
|
4
28
|
app.post('/api/stripe/create-checkout-session', async (req, reply) => {
|
|
5
29
|
const body = req.body;
|
|
@@ -43,13 +67,13 @@ export function registerStripeRoutes(app) {
|
|
|
43
67
|
stripeCustomerId = stripeCustomer.id;
|
|
44
68
|
upsertCustomer(email, stripeCustomerId);
|
|
45
69
|
}
|
|
46
|
-
const publicUrl = process.env.PUBLIC_URL || '
|
|
70
|
+
const publicUrl = process.env.PUBLIC_URL || 'https://content-grade.onrender.com';
|
|
47
71
|
const session = await stripe.checkout.sessions.create({
|
|
48
72
|
mode,
|
|
49
73
|
customer: stripeCustomerId,
|
|
50
74
|
client_reference_id: email,
|
|
51
75
|
line_items: [{ price: priceId, quantity: 1 }],
|
|
52
|
-
success_url: body?.successUrl ?? `${publicUrl}?
|
|
76
|
+
success_url: body?.successUrl ?? `${publicUrl}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
53
77
|
cancel_url: body?.cancelUrl ?? `${publicUrl}?checkout=cancel`,
|
|
54
78
|
});
|
|
55
79
|
return { url: session.url };
|
|
@@ -88,6 +112,9 @@ export function registerStripeRoutes(app) {
|
|
|
88
112
|
if (event.type === 'checkout.session.completed') {
|
|
89
113
|
const email = (data.client_reference_id ?? data.customer_email ?? '').toLowerCase();
|
|
90
114
|
const stripeCustomerId = data.customer;
|
|
115
|
+
if (!email) {
|
|
116
|
+
console.error('[stripe_webhook] checkout.session.completed missing customer email — session:', data.id, '— license key NOT generated');
|
|
117
|
+
}
|
|
91
118
|
if (email && stripeCustomerId) {
|
|
92
119
|
upsertCustomer(email, stripeCustomerId);
|
|
93
120
|
}
|
|
@@ -158,7 +185,7 @@ export function registerStripeRoutes(app) {
|
|
|
158
185
|
return { error: 'No Stripe customer found for this email. Please use the email you used to subscribe.' };
|
|
159
186
|
}
|
|
160
187
|
try {
|
|
161
|
-
const publicUrl = process.env.PUBLIC_URL || '
|
|
188
|
+
const publicUrl = process.env.PUBLIC_URL || 'https://content-grade.onrender.com';
|
|
162
189
|
const session = await stripe.billingPortal.sessions.create({
|
|
163
190
|
customer: customerId,
|
|
164
191
|
return_url: body?.returnUrl ?? publicUrl,
|
|
@@ -202,6 +229,58 @@ export function registerStripeRoutes(app) {
|
|
|
202
229
|
}
|
|
203
230
|
return { valid: true, email: result.email, productKey: result.productKey };
|
|
204
231
|
});
|
|
232
|
+
// Post-checkout success page — Stripe redirects here after payment.
|
|
233
|
+
// Retrieves the session, looks up (or generates) the license key, and presents it to the customer.
|
|
234
|
+
app.get('/checkout/success', async (req, reply) => {
|
|
235
|
+
const query = req.query;
|
|
236
|
+
const sessionId = (query.session_id ?? '').trim();
|
|
237
|
+
if (!sessionId) {
|
|
238
|
+
reply.type('text/html').status(400);
|
|
239
|
+
return successHtml('Something went wrong', '<p>No checkout session found. Please contact support at content-grade.onrender.com.</p>');
|
|
240
|
+
}
|
|
241
|
+
const stripe = getStripe();
|
|
242
|
+
if (!stripe) {
|
|
243
|
+
reply.type('text/html').status(503);
|
|
244
|
+
return successHtml('Configuration error', '<p>Payment system unavailable. Please contact support.</p>');
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const session = await stripe.checkout.sessions.retrieve(sessionId);
|
|
248
|
+
if (session.payment_status !== 'paid') {
|
|
249
|
+
reply.type('text/html').status(402);
|
|
250
|
+
return successHtml('Payment pending', '<p>Your payment has not completed yet. Please wait a moment and refresh.</p>');
|
|
251
|
+
}
|
|
252
|
+
const email = (session.client_reference_id ?? session.customer_email ?? '').toLowerCase();
|
|
253
|
+
if (!email) {
|
|
254
|
+
reply.type('text/html').status(400);
|
|
255
|
+
return successHtml('Email not found', '<p>We couldn\'t identify your account. Please contact support with your Stripe receipt.</p>');
|
|
256
|
+
}
|
|
257
|
+
const customerId = typeof session.customer === 'string' ? session.customer : undefined;
|
|
258
|
+
const licenseKey = upsertLicenseKey(email, customerId, 'contentgrade_pro');
|
|
259
|
+
reply.type('text/html');
|
|
260
|
+
return successHtml('You\'re now Pro 🎉', `
|
|
261
|
+
<p>Payment confirmed for <strong>${email}</strong>.</p>
|
|
262
|
+
<p>Your license key:</p>
|
|
263
|
+
<pre style="background:#111;color:#7fff7f;padding:16px;border-radius:6px;font-size:1.1em;letter-spacing:0.05em">${licenseKey}</pre>
|
|
264
|
+
<p>Activate it in your terminal:</p>
|
|
265
|
+
<pre style="background:#111;color:#ccc;padding:12px;border-radius:6px">content-grade activate ${licenseKey}</pre>
|
|
266
|
+
<p style="font-size:0.9em;color:#888">
|
|
267
|
+
Save this key — it won't be shown again.<br>
|
|
268
|
+
Retrieve it any time: <code>GET /api/stripe/license-key?email=${encodeURIComponent(email)}</code>
|
|
269
|
+
</p>
|
|
270
|
+
`);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
console.error('[checkout_success]', err.message);
|
|
274
|
+
reply.type('text/html').status(500);
|
|
275
|
+
return successHtml('Error retrieving order', '<p>We could not load your order details. Please contact support with your Stripe receipt.</p>');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
// Upgrade redirect — CLI rate-limit messages point here for a clean, stable URL
|
|
279
|
+
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);
|
|
283
|
+
});
|
|
205
284
|
app.get('/api/stripe/subscription-status', async (req, reply) => {
|
|
206
285
|
const query = req.query;
|
|
207
286
|
const email = (query.email ?? '').trim().toLowerCase();
|
package/package.json
CHANGED