thumbgate 1.27.19 → 1.27.20
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/.claude/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +0 -3
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bin/cli.js +27 -1
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +14 -23
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/diagnostic.html +345 -0
- package/public/index.html +2 -2
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/numbers.html +2 -2
- package/public/pricing.html +399 -0
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/classifier-routing.js +130 -0
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/install-shim.js +87 -0
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1707
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* verify-marketing-pages-deployed.js — post-deploy probe for top-level
|
|
6
|
+
* marketing pages.
|
|
7
|
+
*
|
|
8
|
+
* Reads `config/post-deploy-marketing-pages.json` and curls every entry
|
|
9
|
+
* against the live production URL (default
|
|
10
|
+
* https://thumbgate-production.up.railway.app, overridable via
|
|
11
|
+
* THUMBGATE_PROD_URL env or --prod-url=…). Each response body must
|
|
12
|
+
* contain the configured sentinel string and must not contain any configured
|
|
13
|
+
* `mustNotContain` strings. Any mismatch fails the run and the route is
|
|
14
|
+
* included in the failure summary.
|
|
15
|
+
*
|
|
16
|
+
* Intended to run as a workflow step in .github/workflows/deploy-verify.yml
|
|
17
|
+
* after the version sentinel check, so the marketing surface is verified
|
|
18
|
+
* in the same gate as /health and /dashboard. Adding a new marketing
|
|
19
|
+
* page? Add it to the JSON manifest — no workflow edit required.
|
|
20
|
+
*
|
|
21
|
+
* Modes:
|
|
22
|
+
* --json machine-readable report on stdout, suitable for piping
|
|
23
|
+
* into the GitHub Actions PR comment step.
|
|
24
|
+
* --quiet suppress per-route lines; only print the final summary.
|
|
25
|
+
*
|
|
26
|
+
* Exit code is 0 when every page passes, 1 if any sentinel is missing
|
|
27
|
+
* or any route returns non-200.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
|
|
33
|
+
const DEFAULT_PROD_URL = 'https://thumbgate-production.up.railway.app';
|
|
34
|
+
const DEFAULT_TIMEOUT_MS = 12000;
|
|
35
|
+
const DEFAULT_MANIFEST_PATH = path.resolve(__dirname, '..', 'config', 'post-deploy-marketing-pages.json');
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv = []) {
|
|
38
|
+
const out = {
|
|
39
|
+
prodUrl: process.env.THUMBGATE_PROD_URL || DEFAULT_PROD_URL,
|
|
40
|
+
manifestPath: DEFAULT_MANIFEST_PATH,
|
|
41
|
+
json: false,
|
|
42
|
+
quiet: false,
|
|
43
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
44
|
+
};
|
|
45
|
+
for (const arg of argv) {
|
|
46
|
+
if (arg === '--json') out.json = true;
|
|
47
|
+
else if (arg === '--quiet') out.quiet = true;
|
|
48
|
+
else if (arg.startsWith('--prod-url=')) out.prodUrl = arg.slice('--prod-url='.length);
|
|
49
|
+
else if (arg.startsWith('--manifest=')) out.manifestPath = arg.slice('--manifest='.length);
|
|
50
|
+
else if (arg.startsWith('--timeout-ms=')) {
|
|
51
|
+
const n = Number(arg.slice('--timeout-ms='.length));
|
|
52
|
+
if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function loadManifest(manifestPath) {
|
|
59
|
+
const raw = fs.readFileSync(manifestPath, 'utf-8');
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
if (!Array.isArray(parsed.pages) || parsed.pages.length === 0) {
|
|
62
|
+
throw new Error(`Manifest has no pages: ${manifestPath}`);
|
|
63
|
+
}
|
|
64
|
+
for (const entry of parsed.pages) {
|
|
65
|
+
if (typeof entry.route !== 'string' || !entry.route.startsWith('/')) {
|
|
66
|
+
throw new Error(`Invalid route in manifest: ${JSON.stringify(entry)}`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof entry.sentinel !== 'string' || entry.sentinel.length === 0) {
|
|
69
|
+
throw new Error(`Invalid sentinel for route ${entry.route}`);
|
|
70
|
+
}
|
|
71
|
+
if (entry.mustNotContain != null) {
|
|
72
|
+
if (!Array.isArray(entry.mustNotContain) || entry.mustNotContain.some((value) => typeof value !== 'string' || value.length === 0)) {
|
|
73
|
+
throw new Error(`Invalid mustNotContain for route ${entry.route}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (entry.userAgent != null && (typeof entry.userAgent !== 'string' || entry.userAgent.length === 0)) {
|
|
77
|
+
throw new Error(`Invalid userAgent for route ${entry.route}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function probePage({ prodUrl, route, sentinel, mustNotContain = [], userAgent, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
84
|
+
if (typeof fetchImpl !== 'function') {
|
|
85
|
+
return { route, ok: false, error: 'fetch_unavailable' };
|
|
86
|
+
}
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
|
+
try {
|
|
90
|
+
const url = `${prodUrl.replace(/\/$/, '')}${route}`;
|
|
91
|
+
const res = await fetchImpl(url, {
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
headers: {
|
|
94
|
+
// A real browser-shaped UA so bot-deflection interstitials do not
|
|
95
|
+
// trigger; this probe is meant to verify the buyer-facing surface.
|
|
96
|
+
'User-Agent': userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
|
|
97
|
+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const body = await res.text().catch(() => '');
|
|
101
|
+
const sentinelPresent = body.includes(sentinel);
|
|
102
|
+
const forbiddenPresent = Array.isArray(mustNotContain)
|
|
103
|
+
? mustNotContain.filter((value) => body.includes(value))
|
|
104
|
+
: [];
|
|
105
|
+
return {
|
|
106
|
+
route,
|
|
107
|
+
url,
|
|
108
|
+
status: res.status,
|
|
109
|
+
ok: res.ok && sentinelPresent && forbiddenPresent.length === 0,
|
|
110
|
+
sentinelPresent,
|
|
111
|
+
forbiddenPresent,
|
|
112
|
+
bytes: body.length,
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
route,
|
|
117
|
+
ok: false,
|
|
118
|
+
error: error?.name === 'AbortError' ? `timeout_after_${timeoutMs}ms` : (error?.message || String(error)),
|
|
119
|
+
};
|
|
120
|
+
} finally {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function runVerification({ prodUrl, manifestPath, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
126
|
+
const manifest = loadManifest(manifestPath);
|
|
127
|
+
const results = [];
|
|
128
|
+
// Sequential is fine — manifest has <20 entries; parallelism would
|
|
129
|
+
// be over-engineered and would risk masking a per-route rate-limit signal.
|
|
130
|
+
for (const entry of manifest.pages) {
|
|
131
|
+
// eslint-disable-next-line no-await-in-loop
|
|
132
|
+
const result = await probePage({
|
|
133
|
+
prodUrl,
|
|
134
|
+
route: entry.route,
|
|
135
|
+
sentinel: entry.sentinel,
|
|
136
|
+
mustNotContain: entry.mustNotContain,
|
|
137
|
+
userAgent: entry.userAgent,
|
|
138
|
+
fetchImpl,
|
|
139
|
+
timeoutMs,
|
|
140
|
+
});
|
|
141
|
+
results.push({ ...result, sentinel: entry.sentinel, description: entry.description });
|
|
142
|
+
}
|
|
143
|
+
const passed = results.filter((r) => r.ok);
|
|
144
|
+
const failed = results.filter((r) => !r.ok);
|
|
145
|
+
return {
|
|
146
|
+
generatedAt: new Date().toISOString(),
|
|
147
|
+
prodUrl,
|
|
148
|
+
manifestVersion: manifest.version,
|
|
149
|
+
totalRoutes: results.length,
|
|
150
|
+
passedCount: passed.length,
|
|
151
|
+
failedCount: failed.length,
|
|
152
|
+
verdict: failed.length === 0 ? 'pass' : 'fail',
|
|
153
|
+
results,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderHuman(report, { quiet = false } = {}) {
|
|
158
|
+
const lines = [];
|
|
159
|
+
if (!quiet) {
|
|
160
|
+
for (const r of report.results) {
|
|
161
|
+
if (r.ok) {
|
|
162
|
+
lines.push(`✅ ${r.route.padEnd(20)} HTTP ${r.status} bytes=${r.bytes} sentinel-OK`);
|
|
163
|
+
} else if (r.error) {
|
|
164
|
+
lines.push(`❌ ${r.route.padEnd(20)} ERROR ${r.error}`);
|
|
165
|
+
} else if (!r.sentinelPresent) {
|
|
166
|
+
lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status} sentinel MISSING (expected: ${JSON.stringify(r.sentinel)})`);
|
|
167
|
+
} else if (Array.isArray(r.forbiddenPresent) && r.forbiddenPresent.length > 0) {
|
|
168
|
+
lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status} forbidden content PRESENT (${r.forbiddenPresent.map((value) => JSON.stringify(value)).join(', ')})`);
|
|
169
|
+
} else {
|
|
170
|
+
lines.push(`❌ ${r.route.padEnd(20)} HTTP ${r.status}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
lines.push(`\nSummary: ${report.passedCount}/${report.totalRoutes} pages passed against ${report.prodUrl} (verdict: ${report.verdict.toUpperCase()})`);
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function main(argv) {
|
|
179
|
+
const args = parseArgs(argv);
|
|
180
|
+
let report;
|
|
181
|
+
try {
|
|
182
|
+
report = await runVerification({
|
|
183
|
+
prodUrl: args.prodUrl,
|
|
184
|
+
manifestPath: args.manifestPath,
|
|
185
|
+
timeoutMs: args.timeoutMs,
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
process.stderr.write(`verify-marketing-pages-deployed FAILED: ${error.message}\n`);
|
|
189
|
+
process.exit(2);
|
|
190
|
+
}
|
|
191
|
+
if (args.json) {
|
|
192
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
193
|
+
} else {
|
|
194
|
+
process.stdout.write(`${renderHuman(report, { quiet: args.quiet })}\n`);
|
|
195
|
+
}
|
|
196
|
+
process.exitCode = report.verdict === 'pass' ? 0 : 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
DEFAULT_PROD_URL,
|
|
201
|
+
DEFAULT_TIMEOUT_MS,
|
|
202
|
+
DEFAULT_MANIFEST_PATH,
|
|
203
|
+
parseArgs,
|
|
204
|
+
loadManifest,
|
|
205
|
+
probePage,
|
|
206
|
+
runVerification,
|
|
207
|
+
renderHuman,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
|
|
211
|
+
main(process.argv.slice(2));
|
|
212
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const STAGE_ORDER = [
|
|
4
|
+
'landing',
|
|
5
|
+
'pricing',
|
|
6
|
+
'checkout_viewed',
|
|
7
|
+
'email_submitted',
|
|
8
|
+
'stripe_redirect',
|
|
9
|
+
'purchase',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function parseTimestamp(row) {
|
|
13
|
+
const raw = row && (row.timestamp || row.receivedAt || row.ts);
|
|
14
|
+
const ms = raw ? Date.parse(raw) : NaN;
|
|
15
|
+
return Number.isFinite(ms) ? ms : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pickText(...values) {
|
|
19
|
+
for (const value of values) {
|
|
20
|
+
const text = String(value || '').trim();
|
|
21
|
+
if (text) return text;
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractSessionId(row) {
|
|
27
|
+
return pickText(
|
|
28
|
+
row.sessionId,
|
|
29
|
+
row.visitorSessionId,
|
|
30
|
+
row.visitor_session_id,
|
|
31
|
+
row.attribution && row.attribution.sessionId,
|
|
32
|
+
row.metadata && row.metadata.sessionId,
|
|
33
|
+
row.stripeSessionId,
|
|
34
|
+
row.traceId,
|
|
35
|
+
row.acquisitionId,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractVisitorId(row) {
|
|
40
|
+
return pickText(
|
|
41
|
+
row.visitorId,
|
|
42
|
+
row.visitor_id,
|
|
43
|
+
row.installId,
|
|
44
|
+
row.attribution && row.attribution.visitorId,
|
|
45
|
+
row.metadata && row.metadata.visitorId,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractPath(row) {
|
|
50
|
+
return pickText(
|
|
51
|
+
row.path,
|
|
52
|
+
row.page,
|
|
53
|
+
row.landingPath,
|
|
54
|
+
row.landing_path,
|
|
55
|
+
row.attribution && row.attribution.landingPath,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function classifyStage(row) {
|
|
60
|
+
const event = String(row.eventType || row.event || '').toLowerCase();
|
|
61
|
+
const stage = String(row.stage || '').toLowerCase();
|
|
62
|
+
const path = extractPath(row).toLowerCase();
|
|
63
|
+
const cta = String(row.ctaId || row.cta_id || '').toLowerCase();
|
|
64
|
+
|
|
65
|
+
if (/purchase|paid|checkout\.session\.completed/.test(event) || stage === 'purchase') return 'purchase';
|
|
66
|
+
if (/stripe_redirect|stripe redirect|redirect started/.test(event) || stage === 'stripe_redirect') return 'stripe_redirect';
|
|
67
|
+
if (/email_submitted|email submitted|checkout_interstitial_cta_clicked/.test(event)) return 'email_submitted';
|
|
68
|
+
if (/checkout/.test(path) || /checkout.*view|checkout pro viewed/.test(event)) return 'checkout_viewed';
|
|
69
|
+
if (/pricing/.test(path) || /pricing/.test(cta) || /pricing_cta/.test(event)) return 'pricing';
|
|
70
|
+
if (/landing|page_view|landing_page_view|discovery/.test(event) || stage === 'discovery') return 'landing';
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stageRank(stage) {
|
|
75
|
+
const index = STAGE_ORDER.indexOf(stage);
|
|
76
|
+
return index === -1 ? -1 : index;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function nextMissingStage(maxStage) {
|
|
80
|
+
const index = stageRank(maxStage);
|
|
81
|
+
if (index < 0) return 'unknown';
|
|
82
|
+
if (maxStage === 'purchase') return 'converted';
|
|
83
|
+
return STAGE_ORDER[index + 1] || 'unknown';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildVisitorJourneySummary({ telemetryRows = [], funnelRows = [], limit = 100 } = {}) {
|
|
87
|
+
const sessions = new Map();
|
|
88
|
+
const stageCounts = Object.fromEntries(STAGE_ORDER.map((stage) => [stage, 0]));
|
|
89
|
+
|
|
90
|
+
function ingest(row, source) {
|
|
91
|
+
const ts = parseTimestamp(row);
|
|
92
|
+
if (ts == null) return;
|
|
93
|
+
const sessionId = extractSessionId(row) || `anonymous:${extractVisitorId(row) || ts}`;
|
|
94
|
+
const visitorId = extractVisitorId(row) || null;
|
|
95
|
+
const stage = classifyStage(row);
|
|
96
|
+
const path = extractPath(row);
|
|
97
|
+
const existing = sessions.get(sessionId) || {
|
|
98
|
+
sessionId,
|
|
99
|
+
visitorId,
|
|
100
|
+
firstSeen: new Date(ts).toISOString(),
|
|
101
|
+
lastSeen: new Date(ts).toISOString(),
|
|
102
|
+
maxStage: null,
|
|
103
|
+
dropoffStage: 'unknown',
|
|
104
|
+
events: [],
|
|
105
|
+
paths: [],
|
|
106
|
+
ctas: [],
|
|
107
|
+
sources: [],
|
|
108
|
+
visitorType: row.visitorType || null,
|
|
109
|
+
utm: {},
|
|
110
|
+
referrerHost: pickText(row.referrerHost, row.referrer_host, row.referrer),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
existing.visitorId = existing.visitorId || visitorId;
|
|
114
|
+
existing.firstSeen = new Date(Math.min(Date.parse(existing.firstSeen), ts)).toISOString();
|
|
115
|
+
existing.lastSeen = new Date(Math.max(Date.parse(existing.lastSeen), ts)).toISOString();
|
|
116
|
+
existing.sources.push(source);
|
|
117
|
+
if (stage) {
|
|
118
|
+
stageCounts[stage] += 1;
|
|
119
|
+
if (stageRank(stage) > stageRank(existing.maxStage)) existing.maxStage = stage;
|
|
120
|
+
}
|
|
121
|
+
if (path && !existing.paths.includes(path)) existing.paths.push(path);
|
|
122
|
+
const cta = pickText(row.ctaId, row.cta_id);
|
|
123
|
+
if (cta && !existing.ctas.includes(cta)) existing.ctas.push(cta);
|
|
124
|
+
for (const key of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']) {
|
|
125
|
+
const value = pickText(row[key], row.attribution && row.attribution[key]);
|
|
126
|
+
if (value && !existing.utm[key]) existing.utm[key] = value;
|
|
127
|
+
}
|
|
128
|
+
existing.events.push({
|
|
129
|
+
timestamp: new Date(ts).toISOString(),
|
|
130
|
+
source,
|
|
131
|
+
eventType: pickText(row.eventType, row.event, row.stage, 'unknown'),
|
|
132
|
+
stage,
|
|
133
|
+
path: path || null,
|
|
134
|
+
ctaId: cta || null,
|
|
135
|
+
});
|
|
136
|
+
sessions.set(sessionId, existing);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const row of telemetryRows) ingest(row, 'telemetry');
|
|
140
|
+
for (const row of funnelRows) ingest(row, 'funnel');
|
|
141
|
+
|
|
142
|
+
const journeys = [...sessions.values()].map((session) => {
|
|
143
|
+
session.sources = [...new Set(session.sources)];
|
|
144
|
+
session.events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
145
|
+
session.maxStage = session.maxStage || 'unknown';
|
|
146
|
+
session.dropoffStage = nextMissingStage(session.maxStage);
|
|
147
|
+
session.eventCount = session.events.length;
|
|
148
|
+
return session;
|
|
149
|
+
}).sort((a, b) => Date.parse(b.lastSeen) - Date.parse(a.lastSeen));
|
|
150
|
+
|
|
151
|
+
const dropoffCounts = {};
|
|
152
|
+
for (const journey of journeys) {
|
|
153
|
+
dropoffCounts[journey.dropoffStage] = (dropoffCounts[journey.dropoffStage] || 0) + 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
generatedAt: new Date().toISOString(),
|
|
158
|
+
sessionCount: journeys.length,
|
|
159
|
+
stageCounts,
|
|
160
|
+
dropoffCounts,
|
|
161
|
+
journeys: journeys.slice(0, Math.max(1, Math.min(Number(limit) || 100, 500))),
|
|
162
|
+
truncated: journeys.length > Math.max(1, Math.min(Number(limit) || 100, 500)),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
STAGE_ORDER,
|
|
168
|
+
buildVisitorJourneySummary,
|
|
169
|
+
classifyStage,
|
|
170
|
+
extractSessionId,
|
|
171
|
+
extractVisitorId,
|
|
172
|
+
};
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "thumbgate-marketplace",
|
|
3
|
-
"version": "1.27.15",
|
|
4
|
-
"owner": {
|
|
5
|
-
"name": "Igor Ganapolsky",
|
|
6
|
-
"email": "ig5973700@gmail.com"
|
|
7
|
-
},
|
|
8
|
-
"plugins": [
|
|
9
|
-
{
|
|
10
|
-
"name": "thumbgate",
|
|
11
|
-
"description": "One 👎 becomes a hard rule the agent cannot bypass. Captures thumbs-down feedback, distills it into PreToolUse Pre-Action Checks, enforced across every future Claude Code session.",
|
|
12
|
-
"longDescription": "ThumbGate is a tripwire, not a memory layer. When you thumbs-down an agent action — a force-push that overwrote a teammate's commit, a destructive SQL drop, a refactor you already rejected — ThumbGate captures the pattern, distills a one-sentence lesson, and installs it as a PreToolUse block. The next time the agent reaches for the same tool call shape, the hook fires before execution and Claude sees your own words back as the reason.\n\nThe enforcement lives in the hook, not in the model's context, so the agent cannot bypass it by forgetting, by being prompted around it, or by switching sessions. Same rule works in Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, and OpenCode.\n\n33 pre-action checks shipped (force-push, destructive SQL, mass-delete, npm publish from wrong branch, deploy without verification). Budget enforcement (action count + time limits) prevents runaway agent sessions. NIST / SOC2 / OWASP / CWE compliance tags on every rule for enterprise teams. DPO training-pair export for teams running their own fine-tunes.\n\nFree tier: 2 feedback captures/day, 10 total captures, 3 active auto-promoted prevention rules, fully offline. Pro ($19/mo): hosted sync across machines and agent runtimes, dashboard, audit export.",
|
|
13
|
-
"source": {
|
|
14
|
-
"source": "npm",
|
|
15
|
-
"package": "thumbgate"
|
|
16
|
-
},
|
|
17
|
-
"version": "1.27.15",
|
|
18
|
-
"author": {
|
|
19
|
-
"name": "Igor Ganapolsky",
|
|
20
|
-
"email": "ig5973700@gmail.com",
|
|
21
|
-
"url": "https://thumbgate.ai"
|
|
22
|
-
},
|
|
23
|
-
"homepage": "https://thumbgate.ai",
|
|
24
|
-
"repository": "https://github.com/IgorGanapolsky/ThumbGate",
|
|
25
|
-
"license": "MIT",
|
|
26
|
-
"category": "developer-tools",
|
|
27
|
-
"tags": [
|
|
28
|
-
"guardrails",
|
|
29
|
-
"pretooluse",
|
|
30
|
-
"hooks",
|
|
31
|
-
"feedback",
|
|
32
|
-
"rlhf",
|
|
33
|
-
"dpo",
|
|
34
|
-
"agent-safety",
|
|
35
|
-
"workflow-hardening"
|
|
36
|
-
],
|
|
37
|
-
"keywords": [
|
|
38
|
-
"guardrails",
|
|
39
|
-
"pretooluse",
|
|
40
|
-
"hooks",
|
|
41
|
-
"feedback",
|
|
42
|
-
"rlhf",
|
|
43
|
-
"dpo",
|
|
44
|
-
"agent-safety",
|
|
45
|
-
"workflow-hardening",
|
|
46
|
-
"claude-desktop",
|
|
47
|
-
"desktop-extension",
|
|
48
|
-
"pre-action-checks",
|
|
49
|
-
"ai-agent-safety",
|
|
50
|
-
"mcp",
|
|
51
|
-
"memory"
|
|
52
|
-
],
|
|
53
|
-
"capabilities": {
|
|
54
|
-
"skills": 2,
|
|
55
|
-
"commands": 5,
|
|
56
|
-
"agents": 1,
|
|
57
|
-
"hooks": 3,
|
|
58
|
-
"mcpServer": "thumbgate serve"
|
|
59
|
-
},
|
|
60
|
-
"installCommand": "/plugin install thumbgate@claude-plugins-official",
|
|
61
|
-
"metadata": {
|
|
62
|
-
"author": "Igor Ganapolsky",
|
|
63
|
-
"homepage": "https://thumbgate.ai",
|
|
64
|
-
"license": "MIT",
|
|
65
|
-
"keywords": [
|
|
66
|
-
"guardrails",
|
|
67
|
-
"pretooluse",
|
|
68
|
-
"hooks",
|
|
69
|
-
"feedback",
|
|
70
|
-
"rlhf",
|
|
71
|
-
"dpo",
|
|
72
|
-
"agent-safety",
|
|
73
|
-
"workflow-hardening",
|
|
74
|
-
"claude-desktop",
|
|
75
|
-
"desktop-extension",
|
|
76
|
-
"pre-action-checks",
|
|
77
|
-
"ai-agent-safety",
|
|
78
|
-
"mcp",
|
|
79
|
-
"memory"
|
|
80
|
-
],
|
|
81
|
-
"category": "developer-tools"
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
]
|
|
85
|
-
}
|