content-grade 1.0.27 → 1.0.28

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.
@@ -91,7 +91,7 @@ function incrementUsage() {
91
91
 
92
92
  // Upgrade links — Free → Pro → Business → Team
93
93
  const UPGRADE_LINKS = {
94
- free: 'https://buy.stripe.com/4gM14p87GeCh9vn9ks8k80a', // Pro $9/mo — direct checkout
94
+ free: 'https://content-grade.github.io/Content-Grade/#pricing', // Pro $9/mo — pricing page first
95
95
  pro: 'https://buy.stripe.com/bJefZjafO2Tz36Z2W48k80b', // Business $29/mo
96
96
  business: 'https://buy.stripe.com/cNiaEZfA8cu9bDv4088k80c', // Team $79/mo
97
97
  };
package/bin/telemetry.js CHANGED
@@ -58,18 +58,32 @@ export function isOptedOut() {
58
58
  return cfg?.telemetryEnabled === false;
59
59
  }
60
60
 
61
+ /**
62
+ * Return run count bucket label from total lifetime runs.
63
+ * Buckets: 1-5, 6-20, 21-50, 50+
64
+ */
65
+ export function getRunCountBucket(totalRuns) {
66
+ if (totalRuns <= 5) return '1-5';
67
+ if (totalRuns <= 20) return '6-20';
68
+ if (totalRuns <= 50) return '21-50';
69
+ return '50+';
70
+ }
71
+
61
72
  /**
62
73
  * Initialise telemetry on CLI startup.
63
- * Returns { installId, isNew, optedOut }.
74
+ * Increments total run count and returns { installId, totalRuns, runCountBucket, isNew, optedOut }.
64
75
  * isNew=true means this is the first time the CLI has run — show the notice.
65
76
  */
66
77
  export function initTelemetry() {
67
- if (isOptedOut()) return { installId: null, isNew: false, optedOut: true };
78
+ if (isOptedOut()) return { installId: null, totalRuns: 0, runCountBucket: '1-5', isNew: false, optedOut: true };
68
79
 
69
80
  const cfg = loadConfig();
70
81
 
71
82
  if (cfg?.installId) {
72
- return { installId: cfg.installId, isNew: false, optedOut: false };
83
+ // Increment lifetime run counter
84
+ const totalRuns = (cfg.totalRuns ?? 0) + 1;
85
+ saveConfig({ ...cfg, totalRuns });
86
+ return { installId: cfg.installId, totalRuns, runCountBucket: getRunCountBucket(totalRuns), isNew: false, optedOut: false };
73
87
  }
74
88
 
75
89
  // First run — generate anonymous install ID, enabled by default (opt-out available)
@@ -79,9 +93,10 @@ export function initTelemetry() {
79
93
  installId,
80
94
  installedAt: new Date().toISOString(),
81
95
  telemetryEnabled: true,
96
+ totalRuns: 1,
82
97
  });
83
98
 
84
- return { installId, isNew: true, optedOut: false };
99
+ return { installId, totalRuns: 1, runCountBucket: '1-5', isNew: true, optedOut: false };
85
100
  }
86
101
 
87
102
  /**
@@ -105,10 +120,12 @@ export function recordEvent(data) {
105
120
  const installId = cfg?.installId;
106
121
  if (!installId) return; // shouldn't happen after initTelemetry, but guard
107
122
 
123
+ const totalRuns = cfg?.totalRuns ?? 1;
108
124
  const event = {
109
125
  ...data,
110
126
  package: 'content-grade',
111
127
  installId,
128
+ run_count_bucket: data.run_count_bucket ?? getRunCountBucket(totalRuns),
112
129
  timestamp: new Date().toISOString(),
113
130
  };
114
131
 
@@ -130,4 +130,8 @@ function migrate(db) {
130
130
  db.exec(`ALTER TABLE cli_telemetry ADD COLUMN is_pro INTEGER`);
131
131
  }
132
132
  catch { }
133
+ try {
134
+ db.exec(`ALTER TABLE cli_telemetry ADD COLUMN run_count_bucket TEXT`);
135
+ }
136
+ catch { }
133
137
  }
@@ -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, 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);
46
+ (install_id, event, command, is_pro, duration_ms, success, exit_code, score, content_type, version, platform, node_version, run_count_bucket)
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, body.run_count_bucket ? String(body.run_count_bucket).slice(0, 10) : null);
49
49
  }
50
50
  catch {
51
51
  // never fail — telemetry is non-critical
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-grade",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
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": {