thumbgate 1.15.0 → 1.16.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.
Files changed (129) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.well-known/llms.txt +5 -5
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +59 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +210 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +157 -8
  12. package/config/evals/agent-safety-eval.json +338 -22
  13. package/config/gates/routine.json +43 -0
  14. package/config/github-about.json +3 -3
  15. package/config/model-candidates.json +131 -0
  16. package/openapi/openapi.yaml +118 -2
  17. package/package.json +55 -48
  18. package/public/blog.html +7 -7
  19. package/public/codex-plugin.html +6 -6
  20. package/public/compare.html +29 -23
  21. package/public/dashboard.html +82 -10
  22. package/public/guide.html +28 -28
  23. package/public/index.html +216 -98
  24. package/public/learn.html +50 -22
  25. package/public/lessons.html +1 -1
  26. package/public/numbers.html +17 -17
  27. package/public/pro.html +82 -18
  28. package/scripts/agent-audit-trace.js +55 -0
  29. package/scripts/agent-memory-lifecycle.js +96 -0
  30. package/scripts/agent-readiness-plan.js +118 -0
  31. package/scripts/agentic-data-pipeline.js +21 -1
  32. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  33. package/scripts/ai-org-governance.js +98 -0
  34. package/scripts/ai-search-distribution.js +43 -0
  35. package/scripts/artifact-agent-plan.js +81 -0
  36. package/scripts/billing.js +27 -8
  37. package/scripts/cli-schema.js +18 -2
  38. package/scripts/code-mode-mcp-plan.js +71 -0
  39. package/scripts/context-engine.js +1 -2
  40. package/scripts/context-manager.js +4 -1
  41. package/scripts/dashboard-render-spec.js +1 -1
  42. package/scripts/dashboard.js +275 -9
  43. package/scripts/decision-journal.js +13 -3
  44. package/scripts/document-workflow-governance.js +62 -0
  45. package/scripts/enterprise-agent-rollout.js +34 -0
  46. package/scripts/experience-replay-governance.js +69 -0
  47. package/scripts/export-hf-dataset.js +1 -1
  48. package/scripts/feedback-loop.js +92 -4
  49. package/scripts/feedback-to-rules.js +17 -23
  50. package/scripts/gates-engine.js +4 -6
  51. package/scripts/growth-campaigns.js +49 -0
  52. package/scripts/harness-selector.js +16 -4
  53. package/scripts/hybrid-supervisor-agent.js +64 -0
  54. package/scripts/inference-cache-policy.js +72 -0
  55. package/scripts/inference-economics.js +53 -0
  56. package/scripts/internal-agent-bootstrap.js +12 -2
  57. package/scripts/knowledge-layer-plan.js +108 -0
  58. package/scripts/lesson-inference.js +183 -44
  59. package/scripts/lesson-search.js +4 -1
  60. package/scripts/llm-client.js +157 -26
  61. package/scripts/mailer/resend-mailer.js +112 -1
  62. package/scripts/mcp-transport-strategy.js +66 -0
  63. package/scripts/memory-store-governance.js +60 -0
  64. package/scripts/meta-agent-loop.js +7 -13
  65. package/scripts/model-access-eligibility.js +38 -0
  66. package/scripts/model-migration-readiness.js +55 -0
  67. package/scripts/operational-integrity.js +96 -3
  68. package/scripts/otel-declarative-config.js +56 -0
  69. package/scripts/perplexity-client.js +1 -1
  70. package/scripts/post-training-governance.js +34 -0
  71. package/scripts/private-core-boundary.js +72 -0
  72. package/scripts/production-agent-readiness.js +40 -0
  73. package/scripts/prompt-eval.js +564 -32
  74. package/scripts/prompt-programs.js +93 -0
  75. package/scripts/provider-action-normalizer.js +585 -0
  76. package/scripts/scaling-law-claims.js +60 -0
  77. package/scripts/security-scanner.js +1 -1
  78. package/scripts/self-distill-agent.js +7 -32
  79. package/scripts/seo-gsd.js +232 -55
  80. package/scripts/skill-rag-router.js +53 -0
  81. package/scripts/spec-gate.js +1 -1
  82. package/scripts/student-consistent-training.js +73 -0
  83. package/scripts/synthetic-data-provenance.js +98 -0
  84. package/scripts/task-context-result.js +81 -0
  85. package/scripts/telemetry-analytics.js +149 -0
  86. package/scripts/thompson-sampling.js +2 -2
  87. package/scripts/token-savings.js +7 -6
  88. package/scripts/token-tco.js +46 -0
  89. package/scripts/tool-registry.js +63 -3
  90. package/scripts/verification-loop.js +10 -1
  91. package/scripts/verifier-scoring.js +71 -0
  92. package/scripts/workflow-sentinel.js +284 -28
  93. package/scripts/workspace-agent-routines.js +118 -0
  94. package/src/api/server.js +381 -120
  95. package/scripts/analytics-report.js +0 -328
  96. package/scripts/autonomous-workflow.js +0 -377
  97. package/scripts/billing-setup.js +0 -109
  98. package/scripts/creator-campaigns.js +0 -239
  99. package/scripts/cross-encoder-reranker.js +0 -235
  100. package/scripts/daemon-manager.js +0 -108
  101. package/scripts/decision-trace.js +0 -354
  102. package/scripts/delegation-runtime.js +0 -896
  103. package/scripts/dispatch-brief.js +0 -159
  104. package/scripts/distribution-surfaces.js +0 -110
  105. package/scripts/feedback-history-distiller.js +0 -382
  106. package/scripts/funnel-analytics.js +0 -35
  107. package/scripts/history-distiller.js +0 -200
  108. package/scripts/hosted-job-launcher.js +0 -256
  109. package/scripts/intent-router.js +0 -392
  110. package/scripts/lesson-reranker.js +0 -263
  111. package/scripts/lesson-retrieval.js +0 -148
  112. package/scripts/managed-lesson-agent.js +0 -183
  113. package/scripts/operational-dashboard.js +0 -103
  114. package/scripts/operational-summary.js +0 -129
  115. package/scripts/operator-artifacts.js +0 -608
  116. package/scripts/optimize-context.js +0 -17
  117. package/scripts/org-dashboard.js +0 -206
  118. package/scripts/partner-orchestration.js +0 -146
  119. package/scripts/predictive-insights.js +0 -356
  120. package/scripts/pulse.js +0 -80
  121. package/scripts/reflector-agent.js +0 -221
  122. package/scripts/sales-pipeline.js +0 -681
  123. package/scripts/session-episode-store.js +0 -329
  124. package/scripts/session-health-sensor.js +0 -242
  125. package/scripts/session-report.js +0 -120
  126. package/scripts/swarm-coordinator.js +0 -81
  127. package/scripts/tool-kpi-tracker.js +0 -12
  128. package/scripts/webhook-delivery.js +0 -62
  129. package/scripts/workflow-sprint-intake.js +0 -475
@@ -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 };