thumbgate 1.5.3 → 1.5.8

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.
@@ -71,6 +71,7 @@ function detectAgent(flagAgent) {
71
71
  if (['codex'].includes(normalized)) return 'codex';
72
72
  if (['gemini'].includes(normalized)) return 'gemini';
73
73
  if (['forge', 'forgecode', 'forge-code'].includes(normalized)) return 'forge';
74
+ if (['cursor'].includes(normalized)) return 'cursor';
74
75
  return null;
75
76
  }
76
77
 
@@ -80,9 +81,65 @@ function detectAgent(flagAgent) {
80
81
  if (fs.existsSync(path.join(home, '.codex'))) return 'codex';
81
82
  if (fs.existsSync(path.join(home, '.gemini'))) return 'gemini';
82
83
  if (fs.existsSync(path.join(process.cwd(), 'forge.yaml'))) return 'forge';
84
+ if (fs.existsSync(path.join(process.cwd(), '.cursor'))) return 'cursor';
83
85
  return null;
84
86
  }
85
87
 
88
+ // --- Cursor wiring ---
89
+ // Cursor uses .cursor/mcp.json in the project root. We write the ThumbGate MCP
90
+ // server config there so Cursor picks up the server on next restart. Cursor's
91
+ // native hook model is different from Claude Code's — we rely on the MCP
92
+ // server's PreToolUse-equivalent enforcement via the gate-check tool.
93
+
94
+ function cursorMcpConfigPath() {
95
+ return path.join(process.cwd(), '.cursor', 'mcp.json');
96
+ }
97
+
98
+ function wireCursorHooks(options = {}) {
99
+ const mcpPath = cursorMcpConfigPath();
100
+ const dir = path.dirname(mcpPath);
101
+ const thumbgateServer = {
102
+ command: 'npx',
103
+ args: ['--yes', '--package', 'thumbgate@latest', 'thumbgate', 'serve'],
104
+ };
105
+
106
+ let existing = { mcpServers: {} };
107
+ if (fs.existsSync(mcpPath)) {
108
+ try {
109
+ existing = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
110
+ if (!existing.mcpServers) existing.mcpServers = {};
111
+ } catch {
112
+ return { changed: false, error: `Could not parse ${mcpPath}` };
113
+ }
114
+ }
115
+
116
+ const before = JSON.stringify(existing.mcpServers.thumbgate || null);
117
+ existing.mcpServers.thumbgate = thumbgateServer;
118
+ const after = JSON.stringify(existing.mcpServers.thumbgate);
119
+
120
+ const addedEntry = {
121
+ lifecycle: 'mcpServers.thumbgate',
122
+ command: `${thumbgateServer.command} ${thumbgateServer.args.join(' ')}`,
123
+ };
124
+
125
+ if (options.dryRun) {
126
+ return {
127
+ changed: before !== after,
128
+ dryRun: true,
129
+ settingsPath: mcpPath,
130
+ added: before === after ? [] : [addedEntry],
131
+ };
132
+ }
133
+
134
+ if (before === after) {
135
+ return { changed: false, settingsPath: mcpPath, added: [] };
136
+ }
137
+
138
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
139
+ fs.writeFileSync(mcpPath, JSON.stringify(existing, null, 2) + '\n');
140
+ return { changed: true, settingsPath: mcpPath, added: [addedEntry] };
141
+ }
142
+
86
143
  // --- Claude Code wiring ---
87
144
 
88
145
  function claudeSettingsPath() {
@@ -543,7 +600,7 @@ function wireHooks(options) {
543
600
  const agent = detectAgent(options.agent);
544
601
  if (!agent) {
545
602
  return {
546
- error: 'Could not detect AI agent. Use --agent=claude-code|codex|gemini|forge',
603
+ error: 'Could not detect AI agent. Use --agent=claude-code|codex|gemini|forge|cursor',
547
604
  agent: null,
548
605
  changed: false,
549
606
  };
@@ -563,6 +620,9 @@ function wireHooks(options) {
563
620
  case 'forge':
564
621
  result = wireForgeHooks(options);
565
622
  break;
623
+ case 'cursor':
624
+ result = wireCursorHooks(options);
625
+ break;
566
626
  default:
567
627
  return { error: `Unsupported agent: ${agent}`, agent, changed: false };
568
628
  }
@@ -971,6 +971,18 @@ function generateDashboard(feedbackDir, options = {}) {
971
971
  const feedbackTimeSeries = computeFeedbackTimeSeries(entries, 30);
972
972
  const lessonPipeline = computeLessonPipeline(feedbackDir, entries, gateStats);
973
973
 
974
+ // Estimated token savings — computed from gate blocked counts using the
975
+ // conservative methodology in scripts/token-savings.js. This is the ONLY
976
+ // place "$ saved" appears that's backed by real gate-block data; the landing
977
+ // page hero uses a hardcoded sample number disclosed as "Sample".
978
+ let tokenSavings = null;
979
+ try {
980
+ const { computeTokenSavings } = require('./token-savings');
981
+ tokenSavings = computeTokenSavings({
982
+ blockedCalls: Number(gateStats.blocked) || 0,
983
+ });
984
+ } catch { /* module missing — skip */ }
985
+
974
986
  // Merge lesson counts into feedbackTimeSeries days
975
987
  for (const day of feedbackTimeSeries.days) {
976
988
  day.lessons = lessonPipeline.lessonsByDay.get(day.dayKey) || 0;
@@ -1018,6 +1030,7 @@ function generateDashboard(feedbackDir, options = {}) {
1018
1030
  liveMetrics,
1019
1031
  predictive,
1020
1032
  feedbackTimeSeries,
1033
+ tokenSavings,
1021
1034
  lessonPipeline: {
1022
1035
  stages: lessonPipeline.stages,
1023
1036
  rates: lessonPipeline.rates,
package/src/api/server.js CHANGED
@@ -1896,7 +1896,9 @@ function renderRobotsTxt(runtimeConfig) {
1896
1896
  function renderSitemapXml(runtimeConfig) {
1897
1897
  const entries = [
1898
1898
  { path: '/', changefreq: 'weekly', priority: '1.0' },
1899
- { path: '/pro', changefreq: 'weekly', priority: '0.9' },
1899
+ // /pro consolidated into /#pro-pitch (2026-04-16) — removed from sitemap
1900
+ // so search engines don't chase the 301 instead of indexing the canonical
1901
+ // homepage directly.
1900
1902
  { path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
1901
1903
  ...THUMBGATE_SEO_SITEMAP_ENTRIES,
1902
1904
  ];
@@ -3453,21 +3455,17 @@ async function addContext(){
3453
3455
  }
3454
3456
 
3455
3457
  if (isGetLikeRequest && pathname === '/pro') {
3456
- try {
3457
- servePublicMarketingPage({
3458
- req,
3459
- res,
3460
- parsed,
3461
- hostedConfig,
3462
- isHeadRequest,
3463
- renderHtml: loadProPageHtml,
3464
- extraTelemetry: {
3465
- pageType: 'pro_landing',
3466
- },
3467
- });
3468
- } catch (err) {
3469
- sendText(res, 500, err.message || 'Pro page unavailable');
3470
- }
3458
+ // Consolidated: /pro content now lives inline on `/` as the #pro-pitch
3459
+ // strip (hero-adjacent pricing card). 301 so external links (README,
3460
+ // plugin manifests, guides, compare pages) pass link equity onto the
3461
+ // single canonical landing page. Query string is preserved so UTM
3462
+ // tracking from inbound campaigns still reaches GA/PostHog on `/`.
3463
+ const redirectTarget = `/#pro-pitch${parsed.search || ''}`;
3464
+ res.writeHead(301, {
3465
+ Location: redirectTarget,
3466
+ 'Cache-Control': 'public, max-age=3600',
3467
+ });
3468
+ res.end();
3471
3469
  return;
3472
3470
  }
3473
3471
 
@@ -3591,7 +3589,7 @@ async function addContext(){
3591
3589
  version: pkg.version,
3592
3590
  status: 'ok',
3593
3591
  docs: 'https://github.com/IgorGanapolsky/ThumbGate',
3594
- endpoints: ['/health', '/dashboard', '/guide', '/compare', '/learn', '/pro', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/documents', '/v1/documents/import', '/v1/documents/{documentId}', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/decisions/evaluate', '/v1/decisions/outcome', '/v1/decisions/metrics', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
3592
+ endpoints: ['/health', '/dashboard', '/guide', '/compare', '/learn', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/documents', '/v1/documents/import', '/v1/documents/{documentId}', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/decisions/evaluate', '/v1/decisions/outcome', '/v1/decisions/metrics', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
3595
3593
  }, {}, {
3596
3594
  headOnly: isHeadRequest,
3597
3595
  });