thumbgate 1.15.0 → 1.16.1
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/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +3 -3
- package/.well-known/llms.txt +5 -5
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +59 -35
- package/adapters/chatgpt/openapi.yaml +118 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +210 -84
- package/adapters/opencode/opencode.json +1 -1
- package/bench/prompt-eval-suite.json +5 -1
- package/bin/cli.js +157 -8
- package/config/evals/agent-safety-eval.json +338 -22
- package/config/gates/routine.json +43 -0
- package/config/github-about.json +3 -3
- package/config/model-candidates.json +131 -0
- package/openapi/openapi.yaml +118 -2
- package/package.json +57 -49
- package/public/blog.html +7 -7
- package/public/codex-plugin.html +6 -6
- package/public/compare.html +29 -23
- package/public/dashboard.html +82 -10
- package/public/guide.html +28 -28
- package/public/index.html +216 -98
- package/public/learn.html +50 -22
- package/public/lessons.html +1 -1
- package/public/numbers.html +17 -17
- package/public/pro.html +82 -18
- package/scripts/agent-audit-trace.js +55 -0
- package/scripts/agent-memory-lifecycle.js +96 -0
- package/scripts/agent-readiness-plan.js +118 -0
- package/scripts/agentic-data-pipeline.js +21 -1
- package/scripts/agents-sdk-sandbox-plan.js +57 -0
- package/scripts/ai-org-governance.js +98 -0
- package/scripts/ai-search-distribution.js +43 -0
- package/scripts/artifact-agent-plan.js +81 -0
- package/scripts/billing.js +27 -8
- package/scripts/cli-schema.js +18 -2
- package/scripts/code-mode-mcp-plan.js +71 -0
- package/scripts/context-engine.js +1 -2
- package/scripts/context-manager.js +4 -1
- package/scripts/dashboard-render-spec.js +1 -1
- package/scripts/dashboard.js +275 -9
- package/scripts/decision-journal.js +13 -3
- package/scripts/document-workflow-governance.js +62 -0
- package/scripts/enterprise-agent-rollout.js +34 -0
- package/scripts/experience-replay-governance.js +69 -0
- package/scripts/export-hf-dataset.js +1 -1
- package/scripts/feedback-loop.js +92 -4
- package/scripts/feedback-to-rules.js +17 -23
- package/scripts/gates-engine.js +4 -6
- package/scripts/growth-campaigns.js +49 -0
- package/scripts/harness-selector.js +16 -4
- package/scripts/hybrid-supervisor-agent.js +64 -0
- package/scripts/inference-cache-policy.js +72 -0
- package/scripts/inference-economics.js +53 -0
- package/scripts/internal-agent-bootstrap.js +12 -2
- package/scripts/knowledge-layer-plan.js +108 -0
- package/scripts/lesson-inference.js +183 -44
- package/scripts/lesson-search.js +4 -1
- package/scripts/llm-client.js +157 -26
- package/scripts/mailer/resend-mailer.js +112 -1
- package/scripts/mcp-transport-strategy.js +66 -0
- package/scripts/memory-store-governance.js +60 -0
- package/scripts/meta-agent-loop.js +7 -13
- package/scripts/model-access-eligibility.js +38 -0
- package/scripts/model-migration-readiness.js +55 -0
- package/scripts/operational-integrity.js +96 -3
- package/scripts/otel-declarative-config.js +56 -0
- package/scripts/perplexity-client.js +1 -1
- package/scripts/post-training-governance.js +34 -0
- package/scripts/private-core-boundary.js +72 -0
- package/scripts/production-agent-readiness.js +40 -0
- package/scripts/prompt-eval.js +564 -32
- package/scripts/prompt-programs.js +93 -0
- package/scripts/provider-action-normalizer.js +585 -0
- package/scripts/scaling-law-claims.js +60 -0
- package/scripts/security-scanner.js +1 -1
- package/scripts/self-distill-agent.js +7 -32
- package/scripts/seo-gsd.js +232 -55
- package/scripts/skill-rag-router.js +53 -0
- package/scripts/spec-gate.js +1 -1
- package/scripts/student-consistent-training.js +73 -0
- package/scripts/synthetic-data-provenance.js +98 -0
- package/scripts/task-context-result.js +81 -0
- package/scripts/telemetry-analytics.js +149 -0
- package/scripts/thompson-sampling.js +2 -2
- package/scripts/token-savings.js +7 -6
- package/scripts/token-tco.js +46 -0
- package/scripts/tool-registry.js +63 -3
- package/scripts/verification-loop.js +10 -1
- package/scripts/verifier-scoring.js +71 -0
- package/scripts/workflow-sentinel.js +284 -28
- package/scripts/workspace-agent-routines.js +118 -0
- package/src/api/server.js +381 -120
- package/scripts/analytics-report.js +0 -328
- package/scripts/autonomous-workflow.js +0 -377
- package/scripts/billing-setup.js +0 -109
- package/scripts/creator-campaigns.js +0 -239
- package/scripts/cross-encoder-reranker.js +0 -235
- package/scripts/daemon-manager.js +0 -108
- package/scripts/decision-trace.js +0 -354
- package/scripts/delegation-runtime.js +0 -896
- package/scripts/dispatch-brief.js +0 -159
- package/scripts/distribution-surfaces.js +0 -110
- package/scripts/feedback-history-distiller.js +0 -382
- package/scripts/funnel-analytics.js +0 -35
- package/scripts/history-distiller.js +0 -200
- package/scripts/hosted-job-launcher.js +0 -256
- package/scripts/intent-router.js +0 -392
- package/scripts/lesson-reranker.js +0 -263
- package/scripts/lesson-retrieval.js +0 -148
- package/scripts/managed-lesson-agent.js +0 -183
- package/scripts/operational-dashboard.js +0 -103
- package/scripts/operational-summary.js +0 -129
- package/scripts/operator-artifacts.js +0 -608
- package/scripts/optimize-context.js +0 -17
- package/scripts/org-dashboard.js +0 -206
- package/scripts/partner-orchestration.js +0 -146
- package/scripts/predictive-insights.js +0 -356
- package/scripts/pulse.js +0 -80
- package/scripts/reflector-agent.js +0 -221
- package/scripts/sales-pipeline.js +0 -681
- package/scripts/session-episode-store.js +0 -329
- package/scripts/session-health-sensor.js +0 -242
- package/scripts/session-report.js +0 -120
- package/scripts/swarm-coordinator.js +0 -81
- package/scripts/tool-kpi-tracker.js +0 -12
- package/scripts/webhook-delivery.js +0 -62
- package/scripts/workflow-sprint-intake.js +0 -475
package/scripts/billing-setup.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* billing-setup.js — Wire up hosted billing for the CFO dashboard
|
|
6
|
-
*
|
|
7
|
-
* Generates a THUMBGATE_OPERATOR_KEY and stores it locally so that
|
|
8
|
-
* `node bin/cli.js cfo --today` pulls live revenue from the production server.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* node scripts/billing-setup.js
|
|
12
|
-
*
|
|
13
|
-
* After running, set the printed key on Railway:
|
|
14
|
-
* railway variables set THUMBGATE_OPERATOR_KEY=<key>
|
|
15
|
-
* Then redeploy (or let Railway auto-deploy).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const crypto = require('node:crypto');
|
|
19
|
-
const fs = require('node:fs');
|
|
20
|
-
const path = require('node:path');
|
|
21
|
-
const os = require('node:os');
|
|
22
|
-
|
|
23
|
-
const LOCAL_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'operator.json');
|
|
24
|
-
const PROD_URL = 'https://thumbgate-production.up.railway.app';
|
|
25
|
-
|
|
26
|
-
function generateOperatorKey() {
|
|
27
|
-
return `tg_op_${crypto.randomBytes(20).toString('hex')}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function loadExistingConfig() {
|
|
31
|
-
try {
|
|
32
|
-
const raw = fs.readFileSync(LOCAL_CONFIG_PATH, 'utf8');
|
|
33
|
-
return JSON.parse(raw);
|
|
34
|
-
} catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function saveConfig(config) {
|
|
40
|
-
const dir = path.dirname(LOCAL_CONFIG_PATH);
|
|
41
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
-
fs.writeFileSync(LOCAL_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function verifyEndpoint(baseUrl, key) {
|
|
46
|
-
try {
|
|
47
|
-
const url = new URL('/v1/billing/summary?window=today', baseUrl);
|
|
48
|
-
const res = await fetch(url, {
|
|
49
|
-
method: 'GET',
|
|
50
|
-
headers: { authorization: `Bearer ${key}`, accept: 'application/json' },
|
|
51
|
-
});
|
|
52
|
-
return { status: res.status, ok: res.ok };
|
|
53
|
-
} catch (err) {
|
|
54
|
-
return { status: null, ok: false, error: err.message };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function main() {
|
|
59
|
-
const existing = loadExistingConfig();
|
|
60
|
-
|
|
61
|
-
if (existing && existing.operatorKey && existing.baseUrl) {
|
|
62
|
-
console.log('\n✓ Operator config already exists at', LOCAL_CONFIG_PATH);
|
|
63
|
-
console.log(' Base URL :', existing.baseUrl);
|
|
64
|
-
console.log(' Operator key:', existing.operatorKey);
|
|
65
|
-
console.log('\nTo regenerate, delete the file and re-run this script.');
|
|
66
|
-
|
|
67
|
-
// Set env for this process so the verify check works
|
|
68
|
-
process.env.THUMBGATE_OPERATOR_KEY = existing.operatorKey;
|
|
69
|
-
process.env.THUMBGATE_BILLING_API_BASE_URL = existing.baseUrl;
|
|
70
|
-
|
|
71
|
-
const check = await verifyEndpoint(existing.baseUrl, existing.operatorKey);
|
|
72
|
-
if (check.ok) {
|
|
73
|
-
console.log('\n✓ Production endpoint responds OK — hosted billing is active.');
|
|
74
|
-
} else if (check.status === 403) {
|
|
75
|
-
console.log('\n⚠ Production endpoint returned 403 — the operator key is not yet set on Railway.');
|
|
76
|
-
console.log('\nSet it now:\n');
|
|
77
|
-
console.log(` railway variables set THUMBGATE_OPERATOR_KEY=${existing.operatorKey}`);
|
|
78
|
-
console.log('\nThen redeploy (Railway will pick it up automatically).');
|
|
79
|
-
} else {
|
|
80
|
-
console.log(`\n⚠ Endpoint check returned status ${check.status || 'error'}: ${check.error || ''}`);
|
|
81
|
-
}
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const key = generateOperatorKey();
|
|
86
|
-
const config = {
|
|
87
|
-
operatorKey: key,
|
|
88
|
-
baseUrl: process.env.THUMBGATE_BILLING_API_BASE_URL || PROD_URL,
|
|
89
|
-
createdAt: new Date().toISOString(),
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
saveConfig(config);
|
|
93
|
-
|
|
94
|
-
console.log('\n✓ Operator key generated and saved to', LOCAL_CONFIG_PATH);
|
|
95
|
-
console.log('\n──────────────────────────────────────────────────────');
|
|
96
|
-
console.log(' THUMBGATE_OPERATOR_KEY =', key);
|
|
97
|
-
console.log('──────────────────────────────────────────────────────');
|
|
98
|
-
console.log('\nSet this key on Railway (one-time):');
|
|
99
|
-
console.log('\n railway variables set THUMBGATE_OPERATOR_KEY=' + key);
|
|
100
|
-
console.log('\nOr paste it into the Railway dashboard under Variables.');
|
|
101
|
-
console.log('\nAfter Railway redeploys, run:\n');
|
|
102
|
-
console.log(' node bin/cli.js cfo --today\n');
|
|
103
|
-
console.log('The CFO dashboard will use live production billing data.');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
main().catch((err) => {
|
|
107
|
-
console.error('billing-setup error:', err.message);
|
|
108
|
-
process.exit(1);
|
|
109
|
-
});
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { resolveHostedBillingConfig } = require('./hosted-config');
|
|
4
|
-
|
|
5
|
-
const DEFAULT_TEAM_SEAT_COUNT = 3;
|
|
6
|
-
const DEFAULT_TOP_CREATORS = 5;
|
|
7
|
-
const CREATOR_CHANNELS = [
|
|
8
|
-
'youtube',
|
|
9
|
-
'x',
|
|
10
|
-
'linkedin',
|
|
11
|
-
'instagram',
|
|
12
|
-
'threads',
|
|
13
|
-
'tiktok',
|
|
14
|
-
];
|
|
15
|
-
const CREATOR_CONTENT_SHAPES = [
|
|
16
|
-
'review',
|
|
17
|
-
'workflow_teardown',
|
|
18
|
-
'before_after_demo',
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
function normalizeText(value) {
|
|
22
|
-
if (value === undefined || value === null) return '';
|
|
23
|
-
return String(value).trim();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function normalizeCreatorHandle(value) {
|
|
27
|
-
return normalizeText(value)
|
|
28
|
-
.replace(/^@+/, '')
|
|
29
|
-
.toLowerCase();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function slugify(value) {
|
|
33
|
-
return normalizeText(value)
|
|
34
|
-
.toLowerCase()
|
|
35
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
36
|
-
.replace(/^-+|-+$/g, '');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function buildCreatorOfferCode(handle, motion = 'pro') {
|
|
40
|
-
const creatorCode = slugify(handle).toUpperCase() || 'CREATOR';
|
|
41
|
-
const motionCode = slugify(motion).toUpperCase() || 'PRO';
|
|
42
|
-
return `${creatorCode}-${motionCode}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function applyAttributionParams(url, attribution = {}) {
|
|
46
|
-
const params = {
|
|
47
|
-
utm_source: attribution.source,
|
|
48
|
-
utm_medium: attribution.utmMedium,
|
|
49
|
-
utm_campaign: attribution.utmCampaign,
|
|
50
|
-
utm_content: attribution.utmContent,
|
|
51
|
-
creator: attribution.creator,
|
|
52
|
-
community: attribution.community,
|
|
53
|
-
post_id: attribution.postId,
|
|
54
|
-
comment_id: attribution.commentId,
|
|
55
|
-
campaign_variant: attribution.campaignVariant,
|
|
56
|
-
offer_code: attribution.offerCode,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
for (const [key, value] of Object.entries(params)) {
|
|
60
|
-
const normalized = normalizeText(value);
|
|
61
|
-
if (normalized) {
|
|
62
|
-
url.searchParams.set(key, normalized);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return url;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function buildCreatorCampaignLinks(options = {}, runtimeConfig = resolveHostedBillingConfig({
|
|
69
|
-
requestOrigin: 'https://thumbgate-production.up.railway.app',
|
|
70
|
-
})) {
|
|
71
|
-
const creator = normalizeCreatorHandle(options.creator || options.handle);
|
|
72
|
-
if (!creator) {
|
|
73
|
-
throw new Error('buildCreatorCampaignLinks requires creator');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const source = slugify(options.source || options.platform || 'youtube');
|
|
77
|
-
if (!source) {
|
|
78
|
-
throw new Error('buildCreatorCampaignLinks requires source');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const campaign = slugify(options.campaign || `creator-${creator}-launch`);
|
|
82
|
-
const contentShape = slugify(options.contentShape || options.variant || 'review');
|
|
83
|
-
const community = normalizeText(options.community);
|
|
84
|
-
const postId = normalizeText(options.postId);
|
|
85
|
-
const commentId = normalizeText(options.commentId);
|
|
86
|
-
const offerCode = normalizeText(options.offerCode) || buildCreatorOfferCode(creator, options.motion || 'pro');
|
|
87
|
-
const teamSeatCount = Math.max(Number.parseInt(String(options.seatCount || DEFAULT_TEAM_SEAT_COUNT), 10) || DEFAULT_TEAM_SEAT_COUNT, DEFAULT_TEAM_SEAT_COUNT);
|
|
88
|
-
|
|
89
|
-
const attribution = {
|
|
90
|
-
creator,
|
|
91
|
-
source,
|
|
92
|
-
utmMedium: normalizeText(options.utmMedium) || 'creator_partnership',
|
|
93
|
-
utmCampaign: campaign,
|
|
94
|
-
utmContent: contentShape,
|
|
95
|
-
community,
|
|
96
|
-
postId,
|
|
97
|
-
commentId,
|
|
98
|
-
campaignVariant: contentShape,
|
|
99
|
-
offerCode,
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const landingUrl = applyAttributionParams(new URL('/', runtimeConfig.appOrigin), attribution).toString();
|
|
103
|
-
|
|
104
|
-
const proCheckoutUrl = applyAttributionParams(new URL('/checkout/pro', runtimeConfig.appOrigin), attribution);
|
|
105
|
-
proCheckoutUrl.searchParams.set('cta_id', 'pricing_pro');
|
|
106
|
-
proCheckoutUrl.searchParams.set('cta_placement', 'creator_partnership');
|
|
107
|
-
proCheckoutUrl.searchParams.set('plan_id', 'pro');
|
|
108
|
-
|
|
109
|
-
const teamCheckoutUrl = applyAttributionParams(new URL('/checkout/pro', runtimeConfig.appOrigin), attribution);
|
|
110
|
-
teamCheckoutUrl.searchParams.set('cta_id', 'pricing_team');
|
|
111
|
-
teamCheckoutUrl.searchParams.set('cta_placement', 'creator_partnership');
|
|
112
|
-
teamCheckoutUrl.searchParams.set('plan_id', 'team');
|
|
113
|
-
teamCheckoutUrl.searchParams.set('billing_cycle', 'monthly');
|
|
114
|
-
teamCheckoutUrl.searchParams.set('seat_count', String(teamSeatCount));
|
|
115
|
-
|
|
116
|
-
const sprintUrl = applyAttributionParams(new URL('/', runtimeConfig.appOrigin), attribution);
|
|
117
|
-
sprintUrl.hash = 'workflow-sprint-intake';
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
creator,
|
|
121
|
-
attribution,
|
|
122
|
-
links: {
|
|
123
|
-
landingUrl,
|
|
124
|
-
proCheckoutUrl: proCheckoutUrl.toString(),
|
|
125
|
-
teamCheckoutUrl: teamCheckoutUrl.toString(),
|
|
126
|
-
sprintUrl: sprintUrl.toString(),
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function getCounterValue(counter = {}, key) {
|
|
132
|
-
return Number(counter && counter[key]) || 0;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function summarizeCreatorPerformance(telemetry = null, billingSummary = null, options = {}) {
|
|
136
|
-
const topN = Math.max(Number.parseInt(String(options.topN || DEFAULT_TOP_CREATORS), 10) || DEFAULT_TOP_CREATORS, 1);
|
|
137
|
-
const creators = new Set();
|
|
138
|
-
const addKeys = (counter) => {
|
|
139
|
-
for (const key of Object.keys(counter || {})) {
|
|
140
|
-
const normalized = normalizeText(key);
|
|
141
|
-
if (normalized && normalized !== 'unknown') creators.add(normalized);
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
addKeys(telemetry && telemetry.visitors && telemetry.visitors.byCreator);
|
|
146
|
-
addKeys(telemetry && telemetry.ctas && telemetry.ctas.byCreator);
|
|
147
|
-
addKeys(telemetry && telemetry.ctas && telemetry.ctas.checkoutStartsByCreator);
|
|
148
|
-
addKeys(billingSummary && billingSummary.attribution && billingSummary.attribution.acquisitionByCreator);
|
|
149
|
-
addKeys(billingSummary && billingSummary.attribution && billingSummary.attribution.paidByCreator);
|
|
150
|
-
addKeys(billingSummary && billingSummary.attribution && billingSummary.attribution.bookedRevenueByCreatorCents);
|
|
151
|
-
addKeys(billingSummary && billingSummary.pipeline && billingSummary.pipeline.workflowSprintLeads && billingSummary.pipeline.workflowSprintLeads.byCreator);
|
|
152
|
-
addKeys(billingSummary && billingSummary.pipeline && billingSummary.pipeline.qualifiedWorkflowSprintLeads && billingSummary.pipeline.qualifiedWorkflowSprintLeads.byCreator);
|
|
153
|
-
|
|
154
|
-
return Array.from(creators)
|
|
155
|
-
.map((creator) => {
|
|
156
|
-
const visitors = getCounterValue(telemetry && telemetry.visitors && telemetry.visitors.byCreator, creator);
|
|
157
|
-
const ctaClicks = getCounterValue(telemetry && telemetry.ctas && telemetry.ctas.byCreator, creator);
|
|
158
|
-
const checkoutStarts = getCounterValue(telemetry && telemetry.ctas && telemetry.ctas.checkoutStartsByCreator, creator);
|
|
159
|
-
const acquisitions = getCounterValue(billingSummary && billingSummary.attribution && billingSummary.attribution.acquisitionByCreator, creator);
|
|
160
|
-
const paidOrders = getCounterValue(billingSummary && billingSummary.attribution && billingSummary.attribution.paidByCreator, creator);
|
|
161
|
-
const bookedRevenueCents = getCounterValue(billingSummary && billingSummary.attribution && billingSummary.attribution.bookedRevenueByCreatorCents, creator);
|
|
162
|
-
const sprintLeads = getCounterValue(
|
|
163
|
-
billingSummary && billingSummary.pipeline && billingSummary.pipeline.workflowSprintLeads && billingSummary.pipeline.workflowSprintLeads.byCreator,
|
|
164
|
-
creator
|
|
165
|
-
);
|
|
166
|
-
const qualifiedSprintLeads = getCounterValue(
|
|
167
|
-
billingSummary && billingSummary.pipeline && billingSummary.pipeline.qualifiedWorkflowSprintLeads && billingSummary.pipeline.qualifiedWorkflowSprintLeads.byCreator,
|
|
168
|
-
creator
|
|
169
|
-
);
|
|
170
|
-
return {
|
|
171
|
-
creator,
|
|
172
|
-
visitors,
|
|
173
|
-
ctaClicks,
|
|
174
|
-
checkoutStarts,
|
|
175
|
-
acquisitions,
|
|
176
|
-
paidOrders,
|
|
177
|
-
bookedRevenueCents,
|
|
178
|
-
sprintLeads,
|
|
179
|
-
qualifiedSprintLeads,
|
|
180
|
-
};
|
|
181
|
-
})
|
|
182
|
-
.sort((left, right) => (
|
|
183
|
-
right.bookedRevenueCents - left.bookedRevenueCents ||
|
|
184
|
-
right.paidOrders - left.paidOrders ||
|
|
185
|
-
right.qualifiedSprintLeads - left.qualifiedSprintLeads ||
|
|
186
|
-
right.checkoutStarts - left.checkoutStarts ||
|
|
187
|
-
right.visitors - left.visitors ||
|
|
188
|
-
left.creator.localeCompare(right.creator)
|
|
189
|
-
))
|
|
190
|
-
.slice(0, topN);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function parseArgs(argv = []) {
|
|
194
|
-
const options = {};
|
|
195
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
196
|
-
const token = argv[index];
|
|
197
|
-
if (!token.startsWith('--')) continue;
|
|
198
|
-
const [key, inlineValue] = token.slice(2).split('=');
|
|
199
|
-
if (inlineValue !== undefined) {
|
|
200
|
-
options[key] = inlineValue;
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
const next = argv[index + 1];
|
|
204
|
-
if (next && !next.startsWith('--')) {
|
|
205
|
-
options[key] = next;
|
|
206
|
-
index += 1;
|
|
207
|
-
} else {
|
|
208
|
-
options[key] = true;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return options;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function runCli(argv = process.argv.slice(2), io = {}) {
|
|
215
|
-
const log = io.log || console.log;
|
|
216
|
-
const error = io.error || console.error;
|
|
217
|
-
const exit = io.exit || process.exit;
|
|
218
|
-
try {
|
|
219
|
-
const options = parseArgs(argv);
|
|
220
|
-
const result = buildCreatorCampaignLinks(options);
|
|
221
|
-
log(JSON.stringify(result, null, 2));
|
|
222
|
-
} catch (err) {
|
|
223
|
-
error(err.message);
|
|
224
|
-
exit(1);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
module.exports = {
|
|
229
|
-
CREATOR_CHANNELS,
|
|
230
|
-
CREATOR_CONTENT_SHAPES,
|
|
231
|
-
buildCreatorCampaignLinks,
|
|
232
|
-
buildCreatorOfferCode,
|
|
233
|
-
normalizeCreatorHandle,
|
|
234
|
-
summarizeCreatorPerformance,
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
if (require.main === module) {
|
|
238
|
-
runCli();
|
|
239
|
-
}
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Cross-Encoder Reranker for ThumbGate lesson retrieval.
|
|
6
|
-
*
|
|
7
|
-
* Two-stage retrieval:
|
|
8
|
-
* Stage 1: Fast candidate retrieval (existing bigram Jaccard + keyword matching)
|
|
9
|
-
* Stage 2: Cross-encoder reranking scores query-document pairs jointly
|
|
10
|
-
*
|
|
11
|
-
* The cross-encoder evaluates the query AND each lesson together (not independently),
|
|
12
|
-
* catching false positives that keyword/vector search misses.
|
|
13
|
-
*
|
|
14
|
-
* Architecture reference: "Advanced RAG Retrieval: Cross-Encoders & Reranking"
|
|
15
|
-
* (Towards Data Science, April 2026)
|
|
16
|
-
*
|
|
17
|
-
* When LLM is available (ANTHROPIC_API_KEY), uses Claude as the cross-encoder.
|
|
18
|
-
* Falls back to enhanced heuristic scoring when LLM is unavailable.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const { retrieveRelevantLessons, scoreRelevance, buildActionSignature } = require('./lesson-retrieval');
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Heuristic cross-encoder: scores a (query, document) pair jointly.
|
|
25
|
-
* Unlike bi-encoder (independent embeddings), this examines the pair together
|
|
26
|
-
* to find semantic relationships that keyword matching misses.
|
|
27
|
-
*/
|
|
28
|
-
function heuristicCrossEncode(query, document) {
|
|
29
|
-
const queryLower = (query || '').toLowerCase();
|
|
30
|
-
const docLower = (document || '').toLowerCase();
|
|
31
|
-
|
|
32
|
-
let score = 0;
|
|
33
|
-
|
|
34
|
-
// 1. Exact substring containment (strongest signal)
|
|
35
|
-
if (queryLower.length > 3 && docLower.length > 3 &&
|
|
36
|
-
(docLower.includes(queryLower) || queryLower.includes(docLower))) {
|
|
37
|
-
score += 0.9;
|
|
38
|
-
return Math.min(score, 1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 2. Shared noun phrases (not just tokens — consecutive word pairs)
|
|
42
|
-
const queryPhrases = extractPhrases(queryLower);
|
|
43
|
-
const docPhrases = extractPhrases(docLower);
|
|
44
|
-
const phraseOverlap = queryPhrases.filter((p) => docPhrases.includes(p));
|
|
45
|
-
score += Math.min(phraseOverlap.length * 0.15, 0.5);
|
|
46
|
-
|
|
47
|
-
// 3. Semantic category matching
|
|
48
|
-
const categories = {
|
|
49
|
-
destructive: ['delete', 'remove', 'drop', 'destroy', 'wipe', 'truncate', 'rm -rf', 'force-push', 'reset --hard'],
|
|
50
|
-
git: ['git', 'push', 'pull', 'merge', 'rebase', 'branch', 'commit', 'checkout', 'stash'],
|
|
51
|
-
database: ['sql', 'query', 'table', 'migration', 'schema', 'database', 'insert', 'update', 'select'],
|
|
52
|
-
deploy: ['deploy', 'release', 'publish', 'railway', 'vercel', 'heroku', 'npm publish'],
|
|
53
|
-
security: ['secret', 'token', 'api key', 'password', 'credential', 'env', '.env', 'pem'],
|
|
54
|
-
file: ['edit', 'write', 'create', 'modify', 'config', 'package.json', 'readme'],
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
for (const [, terms] of Object.entries(categories)) {
|
|
58
|
-
const queryHit = terms.some((t) => queryLower.includes(t));
|
|
59
|
-
const docHit = terms.some((t) => docLower.includes(t));
|
|
60
|
-
if (queryHit && docHit) {
|
|
61
|
-
score += 0.25;
|
|
62
|
-
break; // Only count strongest category match
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 4. Action-target alignment (e.g., "git push" in query matches "push to main" in doc)
|
|
67
|
-
const queryVerbs = extractVerbs(queryLower);
|
|
68
|
-
const docVerbs = extractVerbs(docLower);
|
|
69
|
-
const verbOverlap = queryVerbs.filter((v) => docVerbs.includes(v));
|
|
70
|
-
score += Math.min(verbOverlap.length * 0.1, 0.3);
|
|
71
|
-
|
|
72
|
-
// 5. Negation alignment (both about what NOT to do)
|
|
73
|
-
const queryNegated = /\b(don'?t|never|avoid|block|prevent|stop)\b/.test(queryLower);
|
|
74
|
-
const docNegated = /\b(don'?t|never|avoid|block|prevent|stop)\b/.test(docLower);
|
|
75
|
-
if (queryNegated && docNegated) score += 0.1;
|
|
76
|
-
|
|
77
|
-
return Math.min(score, 1);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* LLM cross-encoder: uses Claude to score relevance of query-document pairs.
|
|
82
|
-
* More accurate but requires API key and costs tokens.
|
|
83
|
-
*/
|
|
84
|
-
async function llmCrossEncode(query, documents) {
|
|
85
|
-
const { isAvailable, callClaude, MODELS } = require('./llm-client');
|
|
86
|
-
if (!isAvailable()) return null;
|
|
87
|
-
|
|
88
|
-
const docList = documents
|
|
89
|
-
.map((d, i) => `[${i}] ${(d.title || '').slice(0, 100)} | ${(d.content || '').slice(0, 200)}`)
|
|
90
|
-
.join('\n');
|
|
91
|
-
|
|
92
|
-
const prompt = `You are a relevance scoring engine. Given a query and a list of documents, score each document's relevance to the query from 0.0 (irrelevant) to 1.0 (highly relevant).
|
|
93
|
-
|
|
94
|
-
Query: "${query.slice(0, 300)}"
|
|
95
|
-
|
|
96
|
-
Documents:
|
|
97
|
-
${docList}
|
|
98
|
-
|
|
99
|
-
Return ONLY a JSON array of scores, one per document. Example: [0.9, 0.2, 0.7, 0.1, 0.5]
|
|
100
|
-
No other text.`;
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const raw = await callClaude({
|
|
104
|
-
systemPrompt: 'You are a relevance scoring engine. Return only JSON arrays of numbers.',
|
|
105
|
-
userPrompt: prompt,
|
|
106
|
-
model: MODELS.FAST,
|
|
107
|
-
maxTokens: 256,
|
|
108
|
-
});
|
|
109
|
-
const scores = JSON.parse(raw);
|
|
110
|
-
if (Array.isArray(scores) && scores.length === documents.length) {
|
|
111
|
-
return scores.map((s) => Math.max(0, Math.min(1, Number(s) || 0)));
|
|
112
|
-
}
|
|
113
|
-
} catch { /* fall back to heuristic */ }
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Two-stage retrieval with cross-encoder reranking.
|
|
119
|
-
*
|
|
120
|
-
* Stage 1: Retrieve top N candidates using existing keyword + bigram matching
|
|
121
|
-
* Stage 2: Rerank candidates using cross-encoder (LLM or heuristic)
|
|
122
|
-
* Return top K results by cross-encoder score
|
|
123
|
-
*/
|
|
124
|
-
async function retrieveWithReranking(toolName, actionContext, options = {}) {
|
|
125
|
-
const {
|
|
126
|
-
candidateCount = 20,
|
|
127
|
-
maxResults = 5,
|
|
128
|
-
useLLM = false,
|
|
129
|
-
feedbackDir,
|
|
130
|
-
} = options;
|
|
131
|
-
|
|
132
|
-
// Stage 1: Fast candidate retrieval (existing system)
|
|
133
|
-
const candidates = retrieveRelevantLessons(toolName, actionContext, {
|
|
134
|
-
maxResults: candidateCount,
|
|
135
|
-
feedbackDir,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
if (candidates.length === 0) return [];
|
|
139
|
-
if (candidates.length <= maxResults) return candidates;
|
|
140
|
-
|
|
141
|
-
const query = `${toolName || ''} ${actionContext || ''}`.trim();
|
|
142
|
-
|
|
143
|
-
// Stage 2: Cross-encoder reranking
|
|
144
|
-
let rerankedScores;
|
|
145
|
-
|
|
146
|
-
if (useLLM) {
|
|
147
|
-
rerankedScores = await llmCrossEncode(query, candidates);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Fall back to heuristic cross-encoder if LLM unavailable or failed
|
|
151
|
-
if (!rerankedScores) {
|
|
152
|
-
rerankedScores = candidates.map((c) => {
|
|
153
|
-
const docText = `${c.title || ''} ${c.content || ''}`;
|
|
154
|
-
return heuristicCrossEncode(query, docText);
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Combine original relevance score with cross-encoder score
|
|
159
|
-
// Weight: 40% original, 60% cross-encoder (cross-encoder is more precise)
|
|
160
|
-
const reranked = candidates.map((c, i) => ({
|
|
161
|
-
...c,
|
|
162
|
-
crossEncoderScore: rerankedScores[i],
|
|
163
|
-
combinedScore: c.relevanceScore * 0.4 + rerankedScores[i] * 0.6,
|
|
164
|
-
}));
|
|
165
|
-
|
|
166
|
-
return reranked
|
|
167
|
-
.sort((a, b) => b.combinedScore - a.combinedScore)
|
|
168
|
-
.slice(0, maxResults);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Synchronous version for use in PreToolUse hooks (cannot be async).
|
|
173
|
-
*/
|
|
174
|
-
function retrieveWithRerankingSync(toolName, actionContext, options = {}) {
|
|
175
|
-
const {
|
|
176
|
-
candidateCount = 20,
|
|
177
|
-
maxResults = 5,
|
|
178
|
-
feedbackDir,
|
|
179
|
-
} = options;
|
|
180
|
-
|
|
181
|
-
const candidates = retrieveRelevantLessons(toolName, actionContext, {
|
|
182
|
-
maxResults: candidateCount,
|
|
183
|
-
feedbackDir,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
if (candidates.length === 0) return [];
|
|
187
|
-
if (candidates.length <= maxResults) return candidates;
|
|
188
|
-
|
|
189
|
-
const query = `${toolName || ''} ${actionContext || ''}`.trim();
|
|
190
|
-
|
|
191
|
-
const rerankedScores = candidates.map((c) => {
|
|
192
|
-
const docText = `${c.title || ''} ${c.content || ''}`;
|
|
193
|
-
return heuristicCrossEncode(query, docText);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
const reranked = candidates.map((c, i) => ({
|
|
197
|
-
...c,
|
|
198
|
-
crossEncoderScore: rerankedScores[i],
|
|
199
|
-
combinedScore: c.relevanceScore * 0.4 + rerankedScores[i] * 0.6,
|
|
200
|
-
}));
|
|
201
|
-
|
|
202
|
-
return reranked
|
|
203
|
-
.sort((a, b) => b.combinedScore - a.combinedScore)
|
|
204
|
-
.slice(0, maxResults);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// --- Utility functions ---
|
|
208
|
-
|
|
209
|
-
function extractPhrases(text) {
|
|
210
|
-
const words = text.split(/\s+/).filter((w) => w.length > 2);
|
|
211
|
-
const phrases = [];
|
|
212
|
-
for (let i = 0; i < words.length - 1; i++) {
|
|
213
|
-
phrases.push(`${words[i]} ${words[i + 1]}`);
|
|
214
|
-
}
|
|
215
|
-
return phrases;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function extractVerbs(text) {
|
|
219
|
-
const verbPatterns = [
|
|
220
|
-
'push', 'pull', 'merge', 'delete', 'create', 'edit', 'write', 'read',
|
|
221
|
-
'deploy', 'install', 'remove', 'run', 'execute', 'build', 'test',
|
|
222
|
-
'commit', 'rebase', 'reset', 'drop', 'truncate', 'migrate', 'publish',
|
|
223
|
-
'block', 'allow', 'approve', 'deny', 'warn', 'log',
|
|
224
|
-
];
|
|
225
|
-
return verbPatterns.filter((v) => text.includes(v));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
module.exports = {
|
|
229
|
-
heuristicCrossEncode,
|
|
230
|
-
llmCrossEncode,
|
|
231
|
-
retrieveWithReranking,
|
|
232
|
-
retrieveWithRerankingSync,
|
|
233
|
-
extractPhrases,
|
|
234
|
-
extractVerbs,
|
|
235
|
-
};
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
|
|
9
|
-
const LABEL = 'com.thumbgate.daemon';
|
|
10
|
-
const PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
11
|
-
const NODE_PATH = process.execPath;
|
|
12
|
-
const GATEWAY_BIN = path.join(__dirname, '..', 'bin', 'cli.js');
|
|
13
|
-
const LOG_DIR = path.join(os.homedir(), '.thumbgate', 'logs');
|
|
14
|
-
|
|
15
|
-
function generatePlist() {
|
|
16
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
17
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
18
|
-
<plist version="1.0">
|
|
19
|
-
<dict>
|
|
20
|
-
<key>Label</key>
|
|
21
|
-
<string>${LABEL}</string>
|
|
22
|
-
<key>ProgramArguments</key>
|
|
23
|
-
<array>
|
|
24
|
-
<string>${NODE_PATH}</string>
|
|
25
|
-
<string>${GATEWAY_BIN}</string>
|
|
26
|
-
<string>serve</string>
|
|
27
|
-
</array>
|
|
28
|
-
<key>RunAtLoad</key>
|
|
29
|
-
<true/>
|
|
30
|
-
<key>KeepAlive</key>
|
|
31
|
-
<dict>
|
|
32
|
-
<key>SuccessfulExit</key>
|
|
33
|
-
<false/>
|
|
34
|
-
</dict>
|
|
35
|
-
<key>StandardOutPath</key>
|
|
36
|
-
<string>${LOG_DIR}/daemon-stdout.log</string>
|
|
37
|
-
<key>StandardErrorPath</key>
|
|
38
|
-
<string>${LOG_DIR}/daemon-stderr.log</string>
|
|
39
|
-
<key>EnvironmentVariables</key>
|
|
40
|
-
<dict>
|
|
41
|
-
<key>PATH</key>
|
|
42
|
-
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
43
|
-
<key>HOME</key>
|
|
44
|
-
<string>${os.homedir()}</string>
|
|
45
|
-
</dict>
|
|
46
|
-
<key>ThrottleInterval</key>
|
|
47
|
-
<integer>10</integer>
|
|
48
|
-
</dict>
|
|
49
|
-
</plist>`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function manageDaemon(subCommand) {
|
|
53
|
-
switch (subCommand) {
|
|
54
|
-
case 'install': {
|
|
55
|
-
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
56
|
-
fs.writeFileSync(PLIST_PATH, generatePlist(), 'utf8');
|
|
57
|
-
try {
|
|
58
|
-
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { stdio: 'pipe' });
|
|
59
|
-
} catch { /* not loaded */ }
|
|
60
|
-
execSync(`launchctl load "${PLIST_PATH}"`, { stdio: 'inherit' });
|
|
61
|
-
console.log(`✅ Daemon installed and started: ${LABEL}`);
|
|
62
|
-
console.log(` Plist: ${PLIST_PATH}`);
|
|
63
|
-
console.log(` Logs: ${LOG_DIR}/daemon-*.log`);
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
case 'uninstall': {
|
|
68
|
-
try {
|
|
69
|
-
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { stdio: 'pipe' });
|
|
70
|
-
} catch { /* not loaded */ }
|
|
71
|
-
if (fs.existsSync(PLIST_PATH)) {
|
|
72
|
-
fs.unlinkSync(PLIST_PATH);
|
|
73
|
-
console.log(`✅ Daemon uninstalled: ${LABEL}`);
|
|
74
|
-
} else {
|
|
75
|
-
console.log('ℹ️ Daemon not installed');
|
|
76
|
-
}
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
case 'restart': {
|
|
81
|
-
try {
|
|
82
|
-
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { stdio: 'pipe' });
|
|
83
|
-
execSync(`launchctl load "${PLIST_PATH}"`, { stdio: 'inherit' });
|
|
84
|
-
console.log(`✅ Daemon restarted: ${LABEL}`);
|
|
85
|
-
} catch (e) {
|
|
86
|
-
console.error(`❌ Restart failed: ${e.message}`);
|
|
87
|
-
}
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
case 'status':
|
|
92
|
-
default: {
|
|
93
|
-
try {
|
|
94
|
-
const output = execSync(`launchctl list 2>/dev/null | grep "${LABEL}"`, { encoding: 'utf8' });
|
|
95
|
-
const parts = output.trim().split(/\s+/);
|
|
96
|
-
const pid = parts[0] === '-' ? 'idle' : `PID ${parts[0]}`;
|
|
97
|
-
const status = parts[1] === '0' ? 'OK' : `exit ${parts[1]}`;
|
|
98
|
-
console.log(`🔧 ThumbGate Daemon: ${pid} (${status})`);
|
|
99
|
-
console.log(` Plist: ${PLIST_PATH}`);
|
|
100
|
-
} catch {
|
|
101
|
-
console.log('ℹ️ Daemon not installed. Run: thumbgate daemon install');
|
|
102
|
-
}
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
module.exports = { manageDaemon, LABEL, PLIST_PATH };
|