thumbgate 1.29.2 → 1.30.0
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-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +78 -7
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +7 -5
- package/config/mcp-allowlists.json +26 -2
- package/config/post-deploy-marketing-pages.json +26 -1
- package/package.json +38 -7
- package/public/architecture.html +130 -0
- package/public/assets/diagrams/agent-integration.png +0 -0
- package/public/assets/diagrams/before-after.svg +21 -0
- package/public/assets/diagrams/decision.svg +36 -0
- package/public/assets/diagrams/feedback-pipeline.png +0 -0
- package/public/assets/diagrams/loop.svg +34 -0
- package/public/assets/diagrams/plugin-topology.png +0 -0
- package/public/assets/diagrams/pre-action-gate-loop.svg +59 -0
- package/public/assets/diagrams/stack.svg +18 -0
- package/public/assets/diagrams/thumbgate-architecture.png +0 -0
- package/public/case-studies.html +151 -0
- package/public/eval-scorecard.html +195 -0
- package/public/eval-scorecard.json +18 -0
- package/public/evaluations.html +168 -0
- package/public/index.html +4 -3
- package/public/numbers.html +2 -2
- package/public/whitepaper.html +189 -0
- package/scripts/activation-quickstart.js +1 -0
- package/scripts/agent-outcome-monitor.js +71 -1
- package/scripts/billing.js +3 -1
- package/scripts/claude-feedback-sync.js +3 -2
- package/scripts/cli-feedback.js +13 -7
- package/scripts/cross-encoder-reranker.js +3 -0
- package/scripts/feedback-aggregate.js +5 -2
- package/scripts/feedback-loop.js +244 -182
- package/scripts/gates-engine.js +81 -4
- package/scripts/generate-case-study-outreach.js +253 -0
- package/scripts/generate-eval-scorecard.js +276 -0
- package/scripts/growth-campaigns.js +183 -0
- package/scripts/jsonl-watcher.js +1 -0
- package/scripts/lesson-inference.js +23 -4
- package/scripts/lesson-retrieval.js +71 -4
- package/scripts/lesson-search.js +26 -3
- package/scripts/mcp-config.js +26 -5
- package/scripts/mcp-oauth.js +37 -2
- package/scripts/model-eval.js +308 -0
- package/scripts/parallel-workflow-orchestrator.js +86 -22
- package/scripts/published-cli.js +11 -1
- package/scripts/refresh-proof-pack.js +261 -0
- package/scripts/risk-scorer.js +144 -15
- package/scripts/statusline-local-stats.js +1 -1
- package/scripts/thumbgate-bench.js +13 -0
- package/scripts/tool-kpi-tracker.js +124 -0
- package/scripts/tool-registry.js +49 -1
- package/src/api/server.js +230 -86
package/src/api/server.js
CHANGED
|
@@ -172,7 +172,8 @@ const {
|
|
|
172
172
|
readTaskOutcomes,
|
|
173
173
|
recordTaskOutcome,
|
|
174
174
|
} = require('../../scripts/task-outcomes');
|
|
175
|
-
const {
|
|
175
|
+
const { monitorProductionSignals } = require('../../scripts/agent-outcome-monitor');
|
|
176
|
+
const { computeToolKpis, recordToolCall } = require('../../scripts/tool-kpi-tracker');
|
|
176
177
|
const {
|
|
177
178
|
calculateEscalationMetrics,
|
|
178
179
|
decideEscalation,
|
|
@@ -237,6 +238,7 @@ const { sendProblem, PROBLEM_TYPES } = require('../../scripts/problem-detail');
|
|
|
237
238
|
const { TOOLS: MCP_TOOLS } = require('../../scripts/tool-registry');
|
|
238
239
|
const mcpOauth = require('../../scripts/mcp-oauth');
|
|
239
240
|
const { packCheckoutReference } = require('../../scripts/checkout-attribution-reference');
|
|
241
|
+
const { normalizeCampaignId } = require('../../scripts/growth-campaigns');
|
|
240
242
|
// OAuth 2.1 (PKCE) authorization-server state for the remote MCP connector
|
|
241
243
|
// (Claude Connectors Directory requires OAuth for authenticated services).
|
|
242
244
|
const oauthStore = mcpOauth.createStore();
|
|
@@ -266,6 +268,11 @@ const CODEX_PLUGIN_PAGE_PATH = path.resolve(__dirname, '../../public/codex-plugi
|
|
|
266
268
|
const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
|
|
267
269
|
const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
|
|
268
270
|
const NUMBERS_PAGE_PATH = path.resolve(__dirname, '../../public/numbers.html');
|
|
271
|
+
const ARCHITECTURE_PAGE_PATH = path.resolve(__dirname, '../../public/architecture.html');
|
|
272
|
+
const WHITEPAPER_PAGE_PATH = path.resolve(__dirname, '../../public/whitepaper.html');
|
|
273
|
+
const EVAL_SCORECARD_PAGE_PATH = path.resolve(__dirname, '../../public/eval-scorecard.html');
|
|
274
|
+
const EVALUATIONS_PAGE_PATH = path.resolve(__dirname, '../../public/evaluations.html');
|
|
275
|
+
const CASE_STUDIES_PAGE_PATH = path.resolve(__dirname, '../../public/case-studies.html');
|
|
269
276
|
const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
|
|
270
277
|
const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
|
|
271
278
|
const ABOUT_PAGE_PATH = path.resolve(__dirname, '../../public/about.html');
|
|
@@ -417,7 +424,7 @@ const BUILD_METADATA = resolveBuildMetadata();
|
|
|
417
424
|
const TERMINAL_JOB_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
418
425
|
const IDLE_JOB_STATUSES = new Set(['queued', 'paused', 'resume_requested']);
|
|
419
426
|
const JOB_CONTROL_ACTIONS = new Set(['pause', 'cancel', 'resume']);
|
|
420
|
-
const TRACKED_LINK_QUERY_KEYS = [
|
|
427
|
+
const TRACKED_LINK_QUERY_KEYS = new Set([
|
|
421
428
|
'utm_source',
|
|
422
429
|
'utm_medium',
|
|
423
430
|
'utm_campaign',
|
|
@@ -445,7 +452,7 @@ const TRACKED_LINK_QUERY_KEYS = [
|
|
|
445
452
|
'seat_count',
|
|
446
453
|
'landing_path',
|
|
447
454
|
'referrer_host',
|
|
448
|
-
];
|
|
455
|
+
]);
|
|
449
456
|
const TRACKED_LINK_TARGETS = Object.freeze({
|
|
450
457
|
gpt: {
|
|
451
458
|
href: 'https://chatgpt.com/g/g-69dcfd1cd5f881918ae31874631d6f08-thumbgate',
|
|
@@ -876,11 +883,13 @@ function updateLessonRecord(feedbackDir, lessonId, updater) {
|
|
|
876
883
|
}
|
|
877
884
|
|
|
878
885
|
function getPublicMcpTools() {
|
|
879
|
-
|
|
886
|
+
const { getExposedTools } = require('../../adapters/mcp/server-stdio');
|
|
887
|
+
return getExposedTools().map((tool) => ({
|
|
880
888
|
name: tool.name,
|
|
881
889
|
...(tool.title ? { title: tool.title } : {}),
|
|
882
890
|
description: tool.description,
|
|
883
891
|
inputSchema: tool.inputSchema,
|
|
892
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
884
893
|
// Serve the tool-registry annotations (readOnlyHint/destructiveHint). Required
|
|
885
894
|
// by the Claude Connectors Directory (missing annotations = the #1 rejection
|
|
886
895
|
// cause) and used by MCP clients for permission prompts. Was being dropped here.
|
|
@@ -889,13 +898,7 @@ function getPublicMcpTools() {
|
|
|
889
898
|
}
|
|
890
899
|
|
|
891
900
|
function getServerCardTools() {
|
|
892
|
-
return
|
|
893
|
-
name: tool.name,
|
|
894
|
-
...(tool.title ? { title: tool.title } : {}),
|
|
895
|
-
description: tool.description,
|
|
896
|
-
inputSchema: tool.inputSchema,
|
|
897
|
-
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
898
|
-
}));
|
|
901
|
+
return getPublicMcpTools();
|
|
899
902
|
}
|
|
900
903
|
|
|
901
904
|
function buildPublicUrl(hostedConfig, pathname) {
|
|
@@ -905,7 +908,7 @@ function buildPublicUrl(hostedConfig, pathname) {
|
|
|
905
908
|
const VERIFICATION_EVIDENCE_URL = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md';
|
|
906
909
|
|
|
907
910
|
function getToolDiscoveryIndex(hostedConfig) {
|
|
908
|
-
return
|
|
911
|
+
return getPublicMcpTools().map((tool) => ({
|
|
909
912
|
name: tool.name,
|
|
910
913
|
description: tool.description,
|
|
911
914
|
annotations: tool.annotations || {},
|
|
@@ -915,7 +918,7 @@ function getToolDiscoveryIndex(hostedConfig) {
|
|
|
915
918
|
|
|
916
919
|
function getContextFootprintReport(hostedConfig) {
|
|
917
920
|
return buildContextFootprintReport({
|
|
918
|
-
tools:
|
|
921
|
+
tools: getPublicMcpTools(),
|
|
919
922
|
schemaUrlTemplate: buildPublicUrl(hostedConfig, '/.well-known/mcp/tools/{name}.json'),
|
|
920
923
|
});
|
|
921
924
|
}
|
|
@@ -1273,7 +1276,8 @@ function inferSearchQuery(referrer) {
|
|
|
1273
1276
|
|
|
1274
1277
|
function getAttributionValue(params, key, fallbackValue) {
|
|
1275
1278
|
const value = params.get(key);
|
|
1276
|
-
|
|
1279
|
+
const resolved = value?.trim() || fallbackValue;
|
|
1280
|
+
return key === 'utm_campaign' ? normalizeCampaignId(resolved) : resolved;
|
|
1277
1281
|
}
|
|
1278
1282
|
|
|
1279
1283
|
function parseUrlSearchParams(urlValue) {
|
|
@@ -1645,17 +1649,28 @@ function projectWorkflowIntakeQueueLead(lead = {}, options = {}) {
|
|
|
1645
1649
|
page: attribution.page || null,
|
|
1646
1650
|
landingPath: attribution.landingPath || null,
|
|
1647
1651
|
},
|
|
1648
|
-
nextOperatorStep: options
|
|
1649
|
-
? 'request_action_time_approval'
|
|
1650
|
-
: options.discoveryPacket?.status === 'approval_ready_not_authorized'
|
|
1651
|
-
? 'request_action_time_approval_for_discovery'
|
|
1652
|
-
: lead.status === 'new' ? 'review_and_qualify' : 'prepare_scope_or_hold',
|
|
1652
|
+
nextOperatorStep: resolveNextOperatorStep(lead, options),
|
|
1653
1653
|
qualificationCard: options.qualificationCard || buildIntakeQualificationCard(lead, options),
|
|
1654
1654
|
discoveryPacket: options.discoveryPacket || buildIntakeDiscoveryPacket(lead, options),
|
|
1655
1655
|
closePacket: options.closePacket || buildIntakeClosePacket(lead, options),
|
|
1656
1656
|
};
|
|
1657
1657
|
}
|
|
1658
1658
|
|
|
1659
|
+
/**
|
|
1660
|
+
* Which action the operator should take next on an intake lead. Extracted from a three-level
|
|
1661
|
+
* nested ternary: the precedence (close packet, then discovery packet, then lead status) is the
|
|
1662
|
+
* actual logic and is far easier to check when it reads top to bottom.
|
|
1663
|
+
*/
|
|
1664
|
+
function resolveNextOperatorStep(lead, options) {
|
|
1665
|
+
if (options.closePacket?.status === 'approval_ready_not_authorized') {
|
|
1666
|
+
return 'request_action_time_approval';
|
|
1667
|
+
}
|
|
1668
|
+
if (options.discoveryPacket?.status === 'approval_ready_not_authorized') {
|
|
1669
|
+
return 'request_action_time_approval_for_discovery';
|
|
1670
|
+
}
|
|
1671
|
+
return lead.status === 'new' ? 'review_and_qualify' : 'prepare_scope_or_hold';
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1659
1674
|
function buildWorkflowIntakeQueue(parsed, workflowSprintIntake, feedbackDir, options = {}) {
|
|
1660
1675
|
const { limit, statuses } = resolveWorkflowIntakeQueueOptions(parsed, workflowSprintIntake);
|
|
1661
1676
|
const leads = workflowSprintIntake.loadWorkflowSprintLeads(feedbackDir);
|
|
@@ -1734,7 +1749,7 @@ function sendInvalidAnalyticsWindowProblem(res, title, err) {
|
|
|
1734
1749
|
}
|
|
1735
1750
|
|
|
1736
1751
|
function sendWorkflowIntakeQueueProblem(res, err) {
|
|
1737
|
-
const status = Number(err
|
|
1752
|
+
const status = Number(err?.statusCode) || 400;
|
|
1738
1753
|
sendProblem(res, {
|
|
1739
1754
|
type: status >= 500 ? PROBLEM_TYPES.INTERNAL : PROBLEM_TYPES.INVALID_REQUEST,
|
|
1740
1755
|
title: status >= 500
|
|
@@ -2689,7 +2704,10 @@ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
|
|
|
2689
2704
|
for (const key of TRACKED_LINK_QUERY_KEYS) {
|
|
2690
2705
|
const value = params.get(key);
|
|
2691
2706
|
if (value && value.trim()) {
|
|
2692
|
-
|
|
2707
|
+
const normalizedValue = key === 'utm_campaign'
|
|
2708
|
+
? normalizeCampaignId(value)
|
|
2709
|
+
: value.trim();
|
|
2710
|
+
destinationUrl.searchParams.set(key, normalizedValue);
|
|
2693
2711
|
}
|
|
2694
2712
|
}
|
|
2695
2713
|
if (target.allowCustomerEmail) {
|
|
@@ -2730,7 +2748,7 @@ function buildTrackedLinkAttribution(target, parsed, req, journeyState, destinat
|
|
|
2730
2748
|
const source = pickFirstText(
|
|
2731
2749
|
params.get('source'),
|
|
2732
2750
|
params.get('utm_source'),
|
|
2733
|
-
target.defaults
|
|
2751
|
+
target.defaults?.utm_source,
|
|
2734
2752
|
inferSource(referrerHost)
|
|
2735
2753
|
);
|
|
2736
2754
|
|
|
@@ -2744,19 +2762,21 @@ function buildTrackedLinkAttribution(target, parsed, req, journeyState, destinat
|
|
|
2744
2762
|
traceId: pickFirstText(params.get('trace_id')),
|
|
2745
2763
|
source,
|
|
2746
2764
|
utmSource: pickFirstText(params.get('utm_source'), source),
|
|
2747
|
-
utmMedium: pickFirstText(params.get('utm_medium'), target.defaults
|
|
2748
|
-
utmCampaign:
|
|
2765
|
+
utmMedium: pickFirstText(params.get('utm_medium'), target.defaults?.utm_medium, 'link_router'),
|
|
2766
|
+
utmCampaign: normalizeCampaignId(
|
|
2767
|
+
pickFirstText(params.get('utm_campaign'), target.defaults?.utm_campaign, 'first_party_redirect')
|
|
2768
|
+
),
|
|
2749
2769
|
utmContent: pickFirstText(params.get('utm_content')),
|
|
2750
2770
|
utmTerm: pickFirstText(params.get('utm_term')),
|
|
2751
2771
|
creator: pickFirstText(params.get('creator'), params.get('creator_handle')),
|
|
2752
2772
|
community: pickFirstText(params.get('community'), params.get('subreddit')),
|
|
2753
2773
|
postId: pickFirstText(params.get('post_id')),
|
|
2754
2774
|
commentId: pickFirstText(params.get('comment_id')),
|
|
2755
|
-
campaignVariant: pickFirstText(params.get('campaign_variant'), target.defaults
|
|
2775
|
+
campaignVariant: pickFirstText(params.get('campaign_variant'), target.defaults?.campaign_variant),
|
|
2756
2776
|
offerCode: pickFirstText(params.get('offer_code')),
|
|
2757
2777
|
ctaId: pickFirstText(params.get('cta_id'), target.ctaId),
|
|
2758
2778
|
ctaPlacement: pickFirstText(params.get('cta_placement'), target.ctaPlacement),
|
|
2759
|
-
planId: pickFirstText(params.get('plan_id'), target.defaults
|
|
2779
|
+
planId: pickFirstText(params.get('plan_id'), target.defaults?.plan_id),
|
|
2760
2780
|
landingPath: pickFirstText(params.get('landing_path'), `/go/${target.slug}`),
|
|
2761
2781
|
page: `/go/${target.slug}`,
|
|
2762
2782
|
referrer,
|
|
@@ -3822,6 +3842,12 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3822
3842
|
{ path: '/codex-enterprise', changefreq: 'weekly', priority: '0.85' },
|
|
3823
3843
|
{ path: '/agents-cost-savings', changefreq: 'weekly', priority: '0.85' },
|
|
3824
3844
|
{ path: '/ai-malpractice-prevention', changefreq: 'weekly', priority: '0.9' },
|
|
3845
|
+
{ path: '/whitepaper', changefreq: 'weekly', priority: '0.9' },
|
|
3846
|
+
{ path: '/architecture', changefreq: 'weekly', priority: '0.85' },
|
|
3847
|
+
{ path: '/eval-scorecard', changefreq: 'weekly', priority: '0.9' },
|
|
3848
|
+
{ path: '/evaluations', changefreq: 'weekly', priority: '0.85' },
|
|
3849
|
+
{ path: '/case-studies', changefreq: 'weekly', priority: '0.9' },
|
|
3850
|
+
{ path: '/numbers', changefreq: 'weekly', priority: '0.8' },
|
|
3825
3851
|
{ path: '/learn/background-agent-control-layer', changefreq: 'weekly', priority: '0.85' },
|
|
3826
3852
|
{ path: '/learn/ac-dc-runtime-enforcement', changefreq: 'weekly', priority: '0.85' },
|
|
3827
3853
|
{ path: '/learn/feedback-loop-vs-decision-layer', changefreq: 'weekly', priority: '0.9' },
|
|
@@ -5273,6 +5299,14 @@ function createApiServer() {
|
|
|
5273
5299
|
? mcpOauth.tokenAudienceValid(oauthSession, resourceUrl)
|
|
5274
5300
|
: rawKeyValid;
|
|
5275
5301
|
if (!authed) {
|
|
5302
|
+
recordToolCall({
|
|
5303
|
+
toolName: msg.params?.name || 'unknown',
|
|
5304
|
+
serverName: 'mcp-http',
|
|
5305
|
+
latencyMs: 0,
|
|
5306
|
+
success: false,
|
|
5307
|
+
agentId: 'unauthenticated',
|
|
5308
|
+
metadata: { denied: true, deniedReason: 'authentication_required' },
|
|
5309
|
+
});
|
|
5276
5310
|
res.writeHead(401, {
|
|
5277
5311
|
'Content-Type': 'application/json',
|
|
5278
5312
|
// RFC 9728: point unauthenticated clients at the resource metadata.
|
|
@@ -5295,6 +5329,14 @@ function createApiServer() {
|
|
|
5295
5329
|
const tool = MCP_TOOLS.find((t) => t.name === name);
|
|
5296
5330
|
const readOnly = Boolean(tool && tool.annotations && tool.annotations.readOnlyHint === true);
|
|
5297
5331
|
if (!readOnly) {
|
|
5332
|
+
recordToolCall({
|
|
5333
|
+
toolName: name || 'unknown',
|
|
5334
|
+
serverName: 'mcp-http',
|
|
5335
|
+
latencyMs: 0,
|
|
5336
|
+
success: false,
|
|
5337
|
+
agentId: 'reviewer',
|
|
5338
|
+
metadata: { denied: true, deniedReason: 'reviewer_read_only' },
|
|
5339
|
+
});
|
|
5298
5340
|
sendJson(res, 200, {
|
|
5299
5341
|
jsonrpc: '2.0', id: msg.id,
|
|
5300
5342
|
error: { code: -32002, message: `Tool "${name}" requires write access; the reviewer credential is read-only.` },
|
|
@@ -5302,15 +5344,39 @@ function createApiServer() {
|
|
|
5302
5344
|
return;
|
|
5303
5345
|
}
|
|
5304
5346
|
}
|
|
5347
|
+
if (oauthSession) {
|
|
5348
|
+
const name = msg.params && msg.params.name;
|
|
5349
|
+
const tool = MCP_TOOLS.find((candidate) => candidate.name === name);
|
|
5350
|
+
const requiredScope = tool?.annotations?.readOnlyHint === true ? 'mcp:read' : 'mcp:write';
|
|
5351
|
+
if (!mcpOauth.scopeAllows(oauthSession, requiredScope)) {
|
|
5352
|
+
recordToolCall({
|
|
5353
|
+
toolName: name || 'unknown',
|
|
5354
|
+
serverName: 'mcp-http',
|
|
5355
|
+
latencyMs: 0,
|
|
5356
|
+
success: false,
|
|
5357
|
+
agentId: oauthSession.clientId || 'oauth-client',
|
|
5358
|
+
metadata: {
|
|
5359
|
+
denied: true,
|
|
5360
|
+
deniedReason: 'insufficient_scope',
|
|
5361
|
+
requiredScope,
|
|
5362
|
+
},
|
|
5363
|
+
});
|
|
5364
|
+
sendJson(res, 200, {
|
|
5365
|
+
jsonrpc: '2.0', id: msg.id,
|
|
5366
|
+
error: { code: -32003, message: `OAuth token is missing required scope "${requiredScope}".` },
|
|
5367
|
+
});
|
|
5368
|
+
return;
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5305
5371
|
(async () => {
|
|
5306
5372
|
try {
|
|
5307
5373
|
const { callTool } = require('../../adapters/mcp/server-stdio');
|
|
5308
|
-
const name = msg.params
|
|
5374
|
+
const name = msg.params?.name;
|
|
5309
5375
|
const args = (msg.params && msg.params.arguments) || {};
|
|
5310
5376
|
const result = await callTool(name, args);
|
|
5311
5377
|
sendJson(res, 200, {
|
|
5312
5378
|
jsonrpc: '2.0', id: msg.id,
|
|
5313
|
-
result
|
|
5379
|
+
result,
|
|
5314
5380
|
});
|
|
5315
5381
|
} catch (err) {
|
|
5316
5382
|
sendJson(res, 200, {
|
|
@@ -5593,7 +5659,7 @@ function createApiServer() {
|
|
|
5593
5659
|
let trackedParsed = parsed;
|
|
5594
5660
|
if (req.method === 'POST') {
|
|
5595
5661
|
const target = getTrackedLinkTarget(trackedLinkMatch[1]);
|
|
5596
|
-
if (!target
|
|
5662
|
+
if (!target?.requiresPost) {
|
|
5597
5663
|
sendJson(res, target ? 405 : 404, target
|
|
5598
5664
|
? { error: 'POST is not supported for this tracked link' }
|
|
5599
5665
|
: { error: 'Tracked link not found', allowed: Object.keys(TRACKED_LINK_TARGETS) }, {
|
|
@@ -5612,7 +5678,7 @@ function createApiServer() {
|
|
|
5612
5678
|
const form = await parseFormBody(req, 16 * 1024);
|
|
5613
5679
|
trackedParsed = new URL(parsed.toString());
|
|
5614
5680
|
for (const [key, value] of Object.entries(form)) {
|
|
5615
|
-
if (key === 'customer_email' || TRACKED_LINK_QUERY_KEYS.
|
|
5681
|
+
if (key === 'customer_email' || TRACKED_LINK_QUERY_KEYS.has(key)) {
|
|
5616
5682
|
trackedParsed.searchParams.set(key, String(value || '').trim());
|
|
5617
5683
|
}
|
|
5618
5684
|
}
|
|
@@ -5684,10 +5750,31 @@ function createApiServer() {
|
|
|
5684
5750
|
return;
|
|
5685
5751
|
}
|
|
5686
5752
|
|
|
5687
|
-
//
|
|
5688
|
-
|
|
5753
|
+
// GET/HEAD only render a confirmation. Capture requires an authenticated
|
|
5754
|
+
// POST so prefetchers and link checkers cannot create human feedback.
|
|
5755
|
+
if ((isGetLikeRequest || req.method === 'POST') && pathname === '/feedback/quick') {
|
|
5689
5756
|
const signal = parsed.searchParams.get('signal');
|
|
5690
5757
|
if (signal === 'up' || signal === 'down') {
|
|
5758
|
+
const emoji = signal === 'up' ? '👍' : '👎';
|
|
5759
|
+
const label = signal === 'up' ? 'Positive' : 'Negative';
|
|
5760
|
+
if (isHeadRequest) {
|
|
5761
|
+
sendHtml(res, 200, '', {}, { headOnly: true });
|
|
5762
|
+
return;
|
|
5763
|
+
}
|
|
5764
|
+
if (req.method === 'GET') {
|
|
5765
|
+
sendHtml(res, 200, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>ThumbGate feedback</title></head>
|
|
5766
|
+
<body style="background:#0a0a0a;color:#fff;font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0">
|
|
5767
|
+
<form method="post" action="/feedback/quick?signal=${signal}" style="text-align:center;background:#141414;padding:40px;border-radius:16px">
|
|
5768
|
+
<div style="font-size:64px">${emoji}</div><h1>Record ${label.toLowerCase()} feedback?</h1>
|
|
5769
|
+
<p>This confirmation prevents previews and crawlers from recording feedback.</p>
|
|
5770
|
+
<button type="submit" style="padding:10px 20px;font-weight:700">Record ${emoji} feedback</button>
|
|
5771
|
+
</form></body></html>`);
|
|
5772
|
+
return;
|
|
5773
|
+
}
|
|
5774
|
+
if (!isAuthorized(req, expectedApiKey)) {
|
|
5775
|
+
sendJson(res, 401, { error: 'Authentication required to record human feedback' });
|
|
5776
|
+
return;
|
|
5777
|
+
}
|
|
5691
5778
|
const chatHistory = readRecentConversationWindow({
|
|
5692
5779
|
feedbackDir: requestSafeDataDir,
|
|
5693
5780
|
limit: 10,
|
|
@@ -5697,35 +5784,17 @@ function createApiServer() {
|
|
|
5697
5784
|
context: 'Quick capture from Claude Code statusline',
|
|
5698
5785
|
chatHistory,
|
|
5699
5786
|
tags: ['statusline', 'quick-capture'],
|
|
5787
|
+
reviewOrigin: 'human',
|
|
5700
5788
|
});
|
|
5701
|
-
const emoji = signal === 'up' ? '👍' : '👎';
|
|
5702
|
-
const color = signal === 'up' ? '#22c55e' : '#ef4444';
|
|
5703
|
-
const label = signal === 'up' ? 'Positive' : 'Negative';
|
|
5704
5789
|
const opposite = signal === 'up' ? 'down' : 'up';
|
|
5705
5790
|
const oppEmoji = signal === 'up' ? '👎' : '👍';
|
|
5706
5791
|
const feedbackId = result.feedbackEvent?.id || 'saved';
|
|
5707
5792
|
const promoted = result.accepted ? 'Promoted to memory' : 'Stored';
|
|
5708
5793
|
sendHtml(res, 200, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>ThumbGate — ${label} feedback</title>
|
|
5709
5794
|
<style>
|
|
5710
|
-
*{box-sizing:border-box}
|
|
5711
5795
|
body{background:#0a0a0a;color:#fff;font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
|
5712
5796
|
.card{text-align:center;background:#141414;border:1px solid #222;border-radius:16px;padding:48px 40px;max-width:420px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,.5)}
|
|
5713
|
-
.emoji{font-size:
|
|
5714
|
-
@keyframes pop{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}}
|
|
5715
|
-
.msg{font-size:22px;color:${color};font-weight:700;margin-bottom:4px}
|
|
5716
|
-
.sub{font-size:13px;color:#666;margin-top:6px;font-family:ui-monospace,monospace}
|
|
5717
|
-
.context-form{margin-top:20px;text-align:left}
|
|
5718
|
-
.context-form label{font-size:12px;color:#888;display:block;margin-bottom:6px}
|
|
5719
|
-
.context-form textarea{width:100%;background:#1a1a1a;border:1px solid #333;border-radius:8px;color:#ccc;padding:10px;font-size:13px;resize:vertical;min-height:60px;font-family:system-ui}
|
|
5720
|
-
.context-form textarea:focus{outline:none;border-color:${color}}
|
|
5721
|
-
.context-form button{margin-top:8px;background:${color};color:#000;border:none;border-radius:8px;padding:8px 20px;font-size:13px;font-weight:600;cursor:pointer}
|
|
5722
|
-
.context-form button:hover{opacity:.85}
|
|
5723
|
-
.actions{margin-top:24px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
|
5724
|
-
.actions a{color:#22d3ee;text-decoration:none;font-size:13px;padding:8px 16px;border:1px solid #333;border-radius:8px;transition:all .15s}
|
|
5725
|
-
.actions a:hover{background:#1a2a2e;border-color:#22d3ee}
|
|
5726
|
-
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#22c55e;color:#000;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:600;display:none;animation:slideUp .3s ease-out}
|
|
5727
|
-
@keyframes slideUp{from{opacity:0;transform:translateX(-50%) translateY(12px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
|
|
5728
|
-
.badge{display:inline-block;font-size:11px;padding:2px 8px;border-radius:4px;background:#1a1a1a;border:1px solid #333;color:#888;margin-top:8px}
|
|
5797
|
+
.emoji{font-size:64px}.sub,label{color:#888}textarea{width:100%;min-height:60px}.actions{margin-top:20px}.actions a{color:#22d3ee;margin:8px}
|
|
5729
5798
|
</style></head><body>
|
|
5730
5799
|
<div class="card">
|
|
5731
5800
|
<div class="emoji">${emoji}</div>
|
|
@@ -5738,7 +5807,7 @@ body{background:#0a0a0a;color:#fff;font-family:system-ui,-apple-system,sans-seri
|
|
|
5738
5807
|
</div>
|
|
5739
5808
|
<div class="actions">
|
|
5740
5809
|
<a href="/lessons/${feedbackId}" title="View the full lesson and edit it">📋 View Lesson</a>
|
|
5741
|
-
<a href="/feedback/quick?signal=${opposite}" title="Meant to click ${oppEmoji}?">
|
|
5810
|
+
<a href="/feedback/quick?signal=${opposite}" title="Meant to click ${oppEmoji}?">Record ${oppEmoji} instead</a>
|
|
5742
5811
|
<a href="/dashboard">Dashboard →</a>
|
|
5743
5812
|
</div>
|
|
5744
5813
|
</div>
|
|
@@ -5762,6 +5831,10 @@ async function addContext(){
|
|
|
5762
5831
|
}
|
|
5763
5832
|
|
|
5764
5833
|
if (req.method === 'POST' && pathname === '/feedback/quick/context') {
|
|
5834
|
+
if (!isAuthorized(req, expectedApiKey)) {
|
|
5835
|
+
sendJson(res, 401, { error: 'Authentication required to update human feedback' });
|
|
5836
|
+
return;
|
|
5837
|
+
}
|
|
5765
5838
|
const body = await parseJsonBody(req);
|
|
5766
5839
|
const signal = body.signal;
|
|
5767
5840
|
const context = typeof body.context === 'string' ? body.context.trim() : '';
|
|
@@ -6206,6 +6279,74 @@ async function addContext(){
|
|
|
6206
6279
|
return;
|
|
6207
6280
|
}
|
|
6208
6281
|
|
|
6282
|
+
if (isGetLikeRequest && (pathname === '/architecture' || pathname === '/architecture.html')) {
|
|
6283
|
+
try {
|
|
6284
|
+
servePublicMarketingPage({
|
|
6285
|
+
req,
|
|
6286
|
+
res,
|
|
6287
|
+
parsed,
|
|
6288
|
+
hostedConfig,
|
|
6289
|
+
isHeadRequest,
|
|
6290
|
+
renderHtml: () => fs.readFileSync(ARCHITECTURE_PAGE_PATH, 'utf-8'),
|
|
6291
|
+
extraTelemetry: { pageType: 'architecture' },
|
|
6292
|
+
});
|
|
6293
|
+
} catch {
|
|
6294
|
+
sendJson(res, 404, { error: 'Architecture page not found' });
|
|
6295
|
+
}
|
|
6296
|
+
return;
|
|
6297
|
+
}
|
|
6298
|
+
|
|
6299
|
+
if (isGetLikeRequest && (pathname === '/whitepaper' || pathname === '/whitepaper.html' || pathname === '/how-we-know' || pathname === '/how-we-know-it-works')) {
|
|
6300
|
+
try {
|
|
6301
|
+
servePublicMarketingPage({
|
|
6302
|
+
req,
|
|
6303
|
+
res,
|
|
6304
|
+
parsed,
|
|
6305
|
+
hostedConfig,
|
|
6306
|
+
isHeadRequest,
|
|
6307
|
+
renderHtml: () => fs.readFileSync(WHITEPAPER_PAGE_PATH, 'utf-8'),
|
|
6308
|
+
extraTelemetry: { pageType: 'whitepaper' },
|
|
6309
|
+
});
|
|
6310
|
+
} catch {
|
|
6311
|
+
sendJson(res, 404, { error: 'White paper page not found' });
|
|
6312
|
+
}
|
|
6313
|
+
return;
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
if (isGetLikeRequest && (pathname === '/eval-scorecard' || pathname === '/eval-scorecard.html' || pathname === '/scorecard' || pathname === '/bench')) {
|
|
6317
|
+
try {
|
|
6318
|
+
servePublicMarketingPage({
|
|
6319
|
+
req,
|
|
6320
|
+
res,
|
|
6321
|
+
parsed,
|
|
6322
|
+
hostedConfig,
|
|
6323
|
+
isHeadRequest,
|
|
6324
|
+
renderHtml: () => fs.readFileSync(EVAL_SCORECARD_PAGE_PATH, 'utf-8'),
|
|
6325
|
+
extraTelemetry: { pageType: 'eval_scorecard' },
|
|
6326
|
+
});
|
|
6327
|
+
} catch {
|
|
6328
|
+
sendJson(res, 404, { error: 'Eval scorecard page not found' });
|
|
6329
|
+
}
|
|
6330
|
+
return;
|
|
6331
|
+
}
|
|
6332
|
+
|
|
6333
|
+
if (isGetLikeRequest && (pathname === '/evaluations' || pathname === '/evaluations.html')) {
|
|
6334
|
+
try {
|
|
6335
|
+
servePublicMarketingPage({
|
|
6336
|
+
req,
|
|
6337
|
+
res,
|
|
6338
|
+
parsed,
|
|
6339
|
+
hostedConfig,
|
|
6340
|
+
isHeadRequest,
|
|
6341
|
+
renderHtml: () => fs.readFileSync(EVALUATIONS_PAGE_PATH, 'utf-8'),
|
|
6342
|
+
extraTelemetry: { pageType: 'evaluations' },
|
|
6343
|
+
});
|
|
6344
|
+
} catch {
|
|
6345
|
+
sendJson(res, 404, { error: 'Evaluations page not found' });
|
|
6346
|
+
}
|
|
6347
|
+
return;
|
|
6348
|
+
}
|
|
6349
|
+
|
|
6209
6350
|
if (isGetLikeRequest && (pathname === '/federal' || pathname === '/federal.html' || pathname === '/government' || pathname === '/gov')) {
|
|
6210
6351
|
// Federal lead-gen page. Routed through servePublicMarketingPage so agency
|
|
6211
6352
|
// arrivals via SBIR / GSA / outbound channels capture UTM attribution and
|
|
@@ -6470,7 +6611,7 @@ async function addContext(){
|
|
|
6470
6611
|
if (
|
|
6471
6612
|
key === 'confirm'
|
|
6472
6613
|
|| key === 'customer_email'
|
|
6473
|
-
|| TRACKED_LINK_QUERY_KEYS.
|
|
6614
|
+
|| TRACKED_LINK_QUERY_KEYS.has(key)
|
|
6474
6615
|
) {
|
|
6475
6616
|
parsed.searchParams.set(key, String(value || '').trim());
|
|
6476
6617
|
}
|
|
@@ -6986,7 +7127,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6986
7127
|
// are configured (production) the key MUST match a configured admin /
|
|
6987
7128
|
// operator / reviewer key — otherwise OAuth would authenticate nobody.
|
|
6988
7129
|
// In insecure/dev mode (no keys configured) any non-empty key is accepted.
|
|
6989
|
-
|
|
7130
|
+
const keyRole = resolveKeyRole(form.get('api_key') || '');
|
|
7131
|
+
if (!keyRole) {
|
|
6990
7132
|
sendJson(res, 400, { error: 'access_denied', error_description: 'invalid ThumbGate API key' });
|
|
6991
7133
|
return;
|
|
6992
7134
|
}
|
|
@@ -6996,6 +7138,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6996
7138
|
codeChallenge: authorizationParams.codeChallenge,
|
|
6997
7139
|
codeChallengeMethod: authorizationParams.codeChallengeMethod,
|
|
6998
7140
|
scope: authorizationParams.scope,
|
|
7141
|
+
allowedScopes: keyRole === 'reviewer' ? ['mcp:read'] : mcpOauth.SUPPORTED_SCOPES,
|
|
6999
7142
|
resource: authorizationParams.resource || buildPublicUrl(hostedConfig, '/mcp'),
|
|
7000
7143
|
boundKey: form.get('api_key') || '',
|
|
7001
7144
|
state,
|
|
@@ -7039,7 +7182,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7039
7182
|
sendJson(res, 200, {
|
|
7040
7183
|
name: 'thumbgate',
|
|
7041
7184
|
version: pkg.version,
|
|
7042
|
-
count:
|
|
7185
|
+
count: getPublicMcpTools().length,
|
|
7043
7186
|
tools: getToolDiscoveryIndex(hostedConfig),
|
|
7044
7187
|
}, {}, {
|
|
7045
7188
|
headOnly: isHeadRequest,
|
|
@@ -7072,7 +7215,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7072
7215
|
});
|
|
7073
7216
|
return;
|
|
7074
7217
|
}
|
|
7075
|
-
const tool =
|
|
7218
|
+
const tool = getPublicMcpTools().find((candidate) => candidate.name === toolName);
|
|
7076
7219
|
if (!tool) {
|
|
7077
7220
|
sendJson(res, 404, {
|
|
7078
7221
|
error: 'tool_not_found',
|
|
@@ -7088,6 +7231,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7088
7231
|
description: tool.description,
|
|
7089
7232
|
annotations: tool.annotations || {},
|
|
7090
7233
|
inputSchema: tool.inputSchema,
|
|
7234
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
7091
7235
|
}, {}, {
|
|
7092
7236
|
headOnly: isHeadRequest,
|
|
7093
7237
|
});
|
|
@@ -8078,29 +8222,25 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8078
8222
|
return;
|
|
8079
8223
|
}
|
|
8080
8224
|
|
|
8081
|
-
// Public case studies — proof
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
<p><a href="https://thumbgate.ai">Home</a> · <a href="/pricing">Pricing</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a> · <a href="/support">Support</a></p>
|
|
8097
|
-
</footer>
|
|
8098
|
-
</body></html>`, {}, {
|
|
8099
|
-
headOnly: isHeadRequest,
|
|
8100
|
-
});
|
|
8225
|
+
// Public case studies — dogfood proof narratives (static HTML; no fabricated logos).
|
|
8226
|
+
if (isGetLikeRequest && (pathname === '/case-studies' || pathname === '/case-studies.html')) {
|
|
8227
|
+
try {
|
|
8228
|
+
servePublicMarketingPage({
|
|
8229
|
+
req,
|
|
8230
|
+
res,
|
|
8231
|
+
parsed,
|
|
8232
|
+
hostedConfig,
|
|
8233
|
+
isHeadRequest,
|
|
8234
|
+
renderHtml: () => fs.readFileSync(CASE_STUDIES_PAGE_PATH, 'utf-8'),
|
|
8235
|
+
extraTelemetry: { pageType: 'case_studies' },
|
|
8236
|
+
});
|
|
8237
|
+
} catch {
|
|
8238
|
+
sendJson(res, 404, { error: 'Case studies page not found' });
|
|
8239
|
+
}
|
|
8101
8240
|
return;
|
|
8102
8241
|
}
|
|
8103
8242
|
|
|
8243
|
+
|
|
8104
8244
|
// Public canonical pricing page. The audit flagged "pricing schizophrenia":
|
|
8105
8245
|
// sales/pricing.json said $49 / $299, COMMERCIAL_TRUTH.md said $19 / $149,
|
|
8106
8246
|
// and there was no buyer-facing surface to reconcile the two. This is now
|
|
@@ -8255,7 +8395,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8255
8395
|
});
|
|
8256
8396
|
return;
|
|
8257
8397
|
}
|
|
8258
|
-
if (result
|
|
8398
|
+
if (result?.retryable === true) {
|
|
8259
8399
|
sendProblem(res, {
|
|
8260
8400
|
type: PROBLEM_TYPES.INTERNAL,
|
|
8261
8401
|
title: 'Fulfillment temporarily unavailable',
|
|
@@ -8443,14 +8583,15 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8443
8583
|
}
|
|
8444
8584
|
|
|
8445
8585
|
const resolvedTraceId = result.traceId || requestedTraceId;
|
|
8446
|
-
|
|
8447
|
-
|
|
8586
|
+
let nextSteps = {};
|
|
8587
|
+
if (result.apiKey) {
|
|
8588
|
+
nextSteps = {
|
|
8448
8589
|
env: `THUMBGATE_API_KEY=${result.apiKey}\nTHUMBGATE_API_BASE_URL=${hostedConfig.billingApiBaseUrl}`,
|
|
8449
8590
|
curl: `curl -X POST ${hostedConfig.billingApiBaseUrl}/v1/feedback/capture \\\n+ -H 'Authorization: Bearer ${result.apiKey}' \\\n+ -H 'Content-Type: application/json' \\\n+ -d '{"signal":"down","context":"example","whatWentWrong":"example","whatToChange":"example"}'`,
|
|
8450
|
-
}
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8591
|
+
};
|
|
8592
|
+
} else if (result.offerKind === 'workflow_hardening_diagnostic') {
|
|
8593
|
+
nextSteps = { fulfillment: 'Payment confirmed. Diagnostic fulfillment is processing; an email will arrive when it completes.' };
|
|
8594
|
+
}
|
|
8454
8595
|
|
|
8455
8596
|
sendJson(res, 200, {
|
|
8456
8597
|
...result,
|
|
@@ -8514,7 +8655,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8514
8655
|
if (req.method === 'GET' && pathname === '/v1/feedback/stats') {
|
|
8515
8656
|
const stats = shouldAggregateFeedback()
|
|
8516
8657
|
? computeAggregateFeedbackStats({ feedbackDir: requestFeedbackPaths.FEEDBACK_DIR })
|
|
8517
|
-
: analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH);
|
|
8658
|
+
: analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH, { humanOnly: true });
|
|
8518
8659
|
try {
|
|
8519
8660
|
const { getStatuslineMeta } = require('../../scripts/statusline-meta');
|
|
8520
8661
|
const meta = getStatuslineMeta({ env: process.env });
|
|
@@ -9034,6 +9175,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9034
9175
|
workflowContract: body.workflowContract,
|
|
9035
9176
|
repoPath: body.repoPath,
|
|
9036
9177
|
localOnly: body.localOnly === true,
|
|
9178
|
+
ttlMs: body.ttlMs,
|
|
9037
9179
|
clear: body.clear === true,
|
|
9038
9180
|
});
|
|
9039
9181
|
sendJson(res, 200, { scope });
|
|
@@ -9116,7 +9258,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9116
9258
|
|
|
9117
9259
|
if (req.method === 'GET' && pathname === '/v1/task-outcomes/monitor') {
|
|
9118
9260
|
const outcomes = readTaskOutcomes({ feedbackDir: requestFeedbackDir });
|
|
9119
|
-
|
|
9261
|
+
const toolKpis = computeToolKpis({ feedbackDir: requestFeedbackDir });
|
|
9262
|
+
sendJson(res, 200, monitorProductionSignals(outcomes, toolKpis));
|
|
9120
9263
|
return;
|
|
9121
9264
|
}
|
|
9122
9265
|
|
|
@@ -9329,6 +9472,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9329
9472
|
guardrails: body.guardrails,
|
|
9330
9473
|
tags: extractTags(body.tags),
|
|
9331
9474
|
skill: body.skill,
|
|
9475
|
+
reviewOrigin: 'human',
|
|
9332
9476
|
});
|
|
9333
9477
|
const actionIntegration = inferActionIntegration(body, req.headers);
|
|
9334
9478
|
appendBestEffortTelemetry(requestFeedbackDir, {
|