thumbgate 0.9.9 → 0.9.11
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/README.md +4 -4
- package/.claude-plugin/marketplace.json +4 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +115 -312
- package/adapters/README.md +2 -2
- package/adapters/amp/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
- package/adapters/chatgpt/openapi.yaml +2 -2
- package/adapters/claude/.mcp.json +3 -3
- package/adapters/codex/config.toml +4 -4
- package/adapters/gemini/function-declarations.json +1 -1
- package/adapters/mcp/server-stdio.js +66 -6
- package/adapters/opencode/opencode.json +4 -2
- package/bin/cli.js +188 -39
- package/config/e2e-critical-flows.json +4 -0
- package/config/gates/default.json +74 -2
- package/config/github-about.json +1 -1
- package/config/mcp-allowlists.json +33 -6
- package/config/skill-packs/react-testing.json +1 -1
- package/config/tessl-tiles.json +3 -3
- package/openapi/openapi.yaml +2 -2
- package/package.json +23 -9
- package/plugins/amp-skill/INSTALL.md +3 -2
- package/plugins/amp-skill/SKILL.md +1 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +5 -3
- package/plugins/claude-codex-bridge/README.md +1 -1
- package/plugins/claude-codex-bridge/skills/setup/SKILL.md +1 -1
- package/plugins/claude-skill/INSTALL.md +4 -3
- package/plugins/claude-skill/SKILL.md +1 -1
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +5 -3
- package/plugins/codex-profile/INSTALL.md +2 -2
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-marketplace/README.md +5 -5
- package/plugins/cursor-marketplace/mcp.json +4 -2
- package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +1 -1
- package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
- package/plugins/gemini-extension/INSTALL.md +4 -4
- package/plugins/opencode-profile/INSTALL.md +5 -5
- package/public/dashboard.html +15 -8
- package/public/index.html +134 -375
- package/public/js/buyer-intent.js +252 -0
- package/public/pro.html +1085 -0
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/adk-consolidator.js +17 -5
- package/scripts/agent-readiness.js +3 -1
- package/scripts/agent-security-hardening.js +4 -4
- package/scripts/auto-promote-gates.js +8 -0
- package/scripts/auto-wire-hooks.js +105 -21
- package/scripts/billing.js +111 -7
- package/scripts/build-metadata.js +14 -0
- package/scripts/check-congruence.js +1 -1
- package/scripts/context-engine.js +2 -1
- package/scripts/daemon-manager.js +2 -2
- package/scripts/dashboard.js +2 -2
- package/scripts/data-governance.js +1 -1
- package/scripts/deploy-gcp.sh +1 -1
- package/scripts/deploy-policy.js +22 -4
- package/scripts/dispatch-brief.js +1 -1
- package/scripts/ensure-repo-bootstrap.js +1 -1
- package/scripts/feedback-attribution.js +22 -10
- package/scripts/feedback-fallback.js +3 -2
- package/scripts/feedback-inbox-read.js +1 -1
- package/scripts/feedback-loop.js +41 -3
- package/scripts/feedback-paths.js +8 -8
- package/scripts/feedback-schema.js +1 -1
- package/scripts/feedback-to-memory.js +2 -2
- package/scripts/filesystem-search.js +2 -2
- package/scripts/gates-engine.js +765 -34
- package/scripts/generate-paperbanana-diagrams.sh +3 -3
- package/scripts/github-about.js +1 -1
- package/scripts/gtm-revenue-loop.js +20 -1
- package/scripts/hook-runtime.js +89 -0
- package/scripts/hook-stop-self-score.sh +3 -3
- package/scripts/hook-thumbgate-cache-updater.js +98 -37
- package/scripts/hosted-config.js +12 -10
- package/scripts/hybrid-feedback-context.js +54 -13
- package/scripts/install-mcp.js +14 -1
- package/scripts/intent-router.js +1 -1
- package/scripts/internal-agent-bootstrap.js +1 -1
- package/scripts/lesson-inference.js +6 -1
- package/scripts/license.js +54 -16
- package/scripts/mcp-config.js +69 -7
- package/scripts/memory-migration.js +1 -1
- package/scripts/money-watcher.js +166 -16
- package/scripts/operational-integrity.js +480 -0
- package/scripts/optimize-context.js +1 -1
- package/scripts/perplexity-marketing.js +1 -1
- package/scripts/post-everywhere.js +7 -12
- package/scripts/post-to-x.js +1 -1
- package/scripts/pr-manager.js +14 -11
- package/scripts/problem-detail.js +10 -10
- package/scripts/profile-router.js +2 -0
- package/scripts/prompt-dlp.js +1 -0
- package/scripts/prove-adapters.js +6 -6
- package/scripts/prove-automation.js +1 -1
- package/scripts/prove-autoresearch.js +1 -1
- package/scripts/prove-claim-verification.js +3 -3
- package/scripts/prove-data-pipeline.js +5 -5
- package/scripts/prove-data-quality.js +1 -1
- package/scripts/prove-evolution.js +7 -7
- package/scripts/prove-harnesses.js +2 -2
- package/scripts/prove-lancedb.js +2 -2
- package/scripts/prove-local-intelligence.js +1 -1
- package/scripts/prove-loop-closure.js +1 -1
- package/scripts/prove-predictive-insights.js +2 -2
- package/scripts/prove-runtime.js +6 -6
- package/scripts/prove-seo-gsd.js +1 -1
- package/scripts/prove-settings.js +4 -4
- package/scripts/prove-subway-upgrades.js +1 -1
- package/scripts/prove-tessl.js +2 -2
- package/scripts/prove-xmemory.js +2 -2
- package/scripts/publish-decision.js +10 -0
- package/scripts/published-cli.js +34 -0
- package/scripts/rate-limiter.js +2 -2
- package/scripts/reddit-monitor-cron.sh +2 -2
- package/scripts/reminder-engine.js +1 -1
- package/scripts/schedule-manager.js +3 -3
- package/scripts/self-healing-check.js +1 -1
- package/scripts/shieldcortex-memory-firewall-runner.mjs +1 -1
- package/scripts/skill-quality-tracker.js +1 -1
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
- package/scripts/social-analytics/engagement-audit.js +202 -0
- package/scripts/social-analytics/generate-instagram-card.js +1 -1
- package/scripts/social-analytics/instagram-thumbgate-post.js +5 -1
- package/scripts/social-analytics/install-growth-automation.js +114 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +8 -2
- package/scripts/social-analytics/publish-thumbgate-launch.js +1 -1
- package/scripts/social-analytics/publishers/reddit.js +7 -12
- package/scripts/social-analytics/publishers/zernio.js +19 -0
- package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
- package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
- package/scripts/social-analytics/sync-launch-assets.js +185 -0
- package/scripts/social-pipeline.js +2 -2
- package/scripts/social-post-hourly.js +185 -0
- package/scripts/social-quality-gate.js +119 -3
- package/scripts/social-reply-monitor.js +150 -34
- package/scripts/statusline-cache-path.js +27 -0
- package/scripts/statusline-meta.js +22 -0
- package/scripts/statusline.sh +24 -32
- package/scripts/sync-version.js +24 -12
- package/scripts/telemetry-analytics.js +4 -4
- package/scripts/tessl-export.js +1 -1
- package/scripts/test-coverage.js +20 -13
- package/scripts/thumbgate-search.js +2 -2
- package/scripts/tool-registry.js +98 -1
- package/scripts/train_from_feedback.py +1 -1
- package/scripts/user-profile.js +4 -4
- package/scripts/validate-feedback.js +1 -1
- package/scripts/vector-store.js +1 -1
- package/scripts/verification-loop.js +1 -1
- package/scripts/verify-run.js +1 -1
- package/scripts/weekly-auto-post.js +1 -1
- package/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
- package/src/api/server.js +291 -41
- package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
- package/scripts/social-analytics/db/social-analytics.db +0 -0
package/src/api/server.js
CHANGED
|
@@ -90,7 +90,15 @@ const {
|
|
|
90
90
|
loadStats: loadGateStats,
|
|
91
91
|
setConstraint,
|
|
92
92
|
loadConstraints,
|
|
93
|
+
setTaskScope,
|
|
94
|
+
setBranchGovernance,
|
|
95
|
+
getScopeState,
|
|
96
|
+
getBranchGovernanceState,
|
|
97
|
+
approveProtectedAction,
|
|
93
98
|
} = require('../../scripts/gates-engine');
|
|
99
|
+
const {
|
|
100
|
+
evaluateOperationalIntegrity,
|
|
101
|
+
} = require('../../scripts/operational-integrity');
|
|
94
102
|
const {
|
|
95
103
|
generateDashboard,
|
|
96
104
|
} = require('../../scripts/dashboard');
|
|
@@ -109,7 +117,7 @@ const {
|
|
|
109
117
|
readJSONLLocal,
|
|
110
118
|
} = require('../../scripts/lesson-synthesis');
|
|
111
119
|
const {
|
|
112
|
-
|
|
120
|
+
searchThumbgate,
|
|
113
121
|
} = require('../../scripts/thumbgate-search');
|
|
114
122
|
const {
|
|
115
123
|
appendTelemetryPing,
|
|
@@ -141,11 +149,13 @@ const {
|
|
|
141
149
|
} = require('../../scripts/seo-gsd');
|
|
142
150
|
|
|
143
151
|
const LANDING_PAGE_PATH = path.resolve(__dirname, '../../public/index.html');
|
|
152
|
+
const PRO_PAGE_PATH = path.resolve(__dirname, '../../public/pro.html');
|
|
144
153
|
const DASHBOARD_PAGE_PATH = path.resolve(__dirname, '../../public/dashboard.html');
|
|
145
154
|
const LESSONS_PAGE_PATH = path.resolve(__dirname, '../../public/lessons.html');
|
|
146
155
|
const GUIDE_PAGE_PATH = path.resolve(__dirname, '../../public/guide.html');
|
|
147
156
|
const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
|
|
148
157
|
const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
|
|
158
|
+
const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
|
|
149
159
|
const VISITOR_COOKIE_NAME = 'thumbgate_visitor_id';
|
|
150
160
|
const SESSION_COOKIE_NAME = 'thumbgate_session_id';
|
|
151
161
|
const ACQUISITION_COOKIE_NAME = 'thumbgate_acquisition_id';
|
|
@@ -215,6 +225,29 @@ function findRecordById(id, feedbackDir) {
|
|
|
215
225
|
return { feedbackEvent, memoryRecord };
|
|
216
226
|
}
|
|
217
227
|
|
|
228
|
+
function mergeFollowUpDetail(existingDetail, followUpText) {
|
|
229
|
+
const existing = normalizeNullableText(existingDetail);
|
|
230
|
+
const next = normalizeNullableText(followUpText);
|
|
231
|
+
if (!next) return existing;
|
|
232
|
+
if (!existing) return next;
|
|
233
|
+
if (existing.includes(next)) return existing;
|
|
234
|
+
return `${existing}\n\nFollow-up: ${next}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function updateLessonRecord(feedbackDir, lessonId, updater) {
|
|
238
|
+
const record = findRecordById(lessonId, feedbackDir);
|
|
239
|
+
if (!record) return null;
|
|
240
|
+
const existing = { ...(record.feedbackEvent || {}), ...(record.memoryRecord || {}) };
|
|
241
|
+
const updated = updater({ ...existing });
|
|
242
|
+
if (!updated) return null;
|
|
243
|
+
const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
|
|
244
|
+
const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
|
|
245
|
+
const updatedMemory = updateRecordInJsonl(memoryLogPath, lessonId, updated);
|
|
246
|
+
const updatedFeedback = updateRecordInJsonl(feedbackLogPath, lessonId, updated);
|
|
247
|
+
if (!updatedMemory && !updatedFeedback) return null;
|
|
248
|
+
return updated;
|
|
249
|
+
}
|
|
250
|
+
|
|
218
251
|
function getPublicMcpTools() {
|
|
219
252
|
return MCP_TOOLS.map((tool) => ({
|
|
220
253
|
name: tool.name,
|
|
@@ -862,8 +895,8 @@ function escapeHtmlAttribute(value) {
|
|
|
862
895
|
.replaceAll('>', '>');
|
|
863
896
|
}
|
|
864
897
|
|
|
865
|
-
function
|
|
866
|
-
const template = fs.readFileSync(
|
|
898
|
+
function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContext = {}) {
|
|
899
|
+
const template = fs.readFileSync(templatePath, 'utf-8');
|
|
867
900
|
const googleSiteVerificationMeta = runtimeConfig.googleSiteVerification
|
|
868
901
|
? ` <meta name="google-site-verification" content="${escapeHtmlAttribute(runtimeConfig.googleSiteVerification)}" />`
|
|
869
902
|
: '';
|
|
@@ -900,6 +933,14 @@ function loadLandingPageHtml(runtimeConfig, pageContext = {}) {
|
|
|
900
933
|
});
|
|
901
934
|
}
|
|
902
935
|
|
|
936
|
+
function loadLandingPageHtml(runtimeConfig, pageContext = {}) {
|
|
937
|
+
return loadPublicMarketingTemplateHtml(LANDING_PAGE_PATH, runtimeConfig, pageContext);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function loadProPageHtml(runtimeConfig, pageContext = {}) {
|
|
941
|
+
return loadPublicMarketingTemplateHtml(PRO_PAGE_PATH, runtimeConfig, pageContext);
|
|
942
|
+
}
|
|
943
|
+
|
|
903
944
|
function loadDashboardPageHtml(req, expectedApiKey) {
|
|
904
945
|
const template = fs.readFileSync(DASHBOARD_PAGE_PATH, 'utf-8');
|
|
905
946
|
const forwardedHost = req.headers['x-forwarded-host'];
|
|
@@ -937,6 +978,13 @@ function loadLessonsPageHtml(req, expectedApiKey) {
|
|
|
937
978
|
|
|
938
979
|
function esc(s) { return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
939
980
|
|
|
981
|
+
function normalizeLessonSignal(signal) {
|
|
982
|
+
const value = String(signal || '').toLowerCase();
|
|
983
|
+
if (value === 'up' || value === 'positive' || value === 'thumbs_up') return 'up';
|
|
984
|
+
if (value === 'down' || value === 'negative' || value === 'thumbs_down') return 'down';
|
|
985
|
+
return 'down';
|
|
986
|
+
}
|
|
987
|
+
|
|
940
988
|
function renderLessonDetailHtml(record, lessonId) {
|
|
941
989
|
if (!record) {
|
|
942
990
|
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Lesson Not Found</title>
|
|
@@ -952,7 +1000,7 @@ a{color:#22d3ee;text-decoration:none}</style></head><body>
|
|
|
952
1000
|
const fb = record.feedbackEvent || {};
|
|
953
1001
|
const mem = record.memoryRecord || {};
|
|
954
1002
|
const merged = { ...fb, ...mem };
|
|
955
|
-
const signal = merged.signal
|
|
1003
|
+
const signal = normalizeLessonSignal(merged.signal);
|
|
956
1004
|
const emoji = signal === 'up' ? '👍' : '👎';
|
|
957
1005
|
const signalColor = signal === 'up' ? '#4ade80' : '#f87171';
|
|
958
1006
|
const title = merged.title || merged.context || 'Untitled Lesson';
|
|
@@ -1228,6 +1276,7 @@ function renderRobotsTxt(runtimeConfig) {
|
|
|
1228
1276
|
function renderSitemapXml(runtimeConfig) {
|
|
1229
1277
|
const entries = [
|
|
1230
1278
|
{ path: '/', changefreq: 'weekly', priority: '1.0' },
|
|
1279
|
+
{ path: '/pro', changefreq: 'weekly', priority: '0.9' },
|
|
1231
1280
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
1232
1281
|
];
|
|
1233
1282
|
return [
|
|
@@ -1550,7 +1599,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
1550
1599
|
}
|
|
1551
1600
|
|
|
1552
1601
|
function sendTelemetryOnce(eventType, extra = {}) {
|
|
1553
|
-
const marker = ['
|
|
1602
|
+
const marker = ['thumbgate', eventType, sessionId || traceId || 'unknown'].join(':');
|
|
1554
1603
|
try {
|
|
1555
1604
|
if (window.sessionStorage && window.sessionStorage.getItem(marker)) {
|
|
1556
1605
|
return;
|
|
@@ -2157,6 +2206,24 @@ function createApiServer() {
|
|
|
2157
2206
|
return;
|
|
2158
2207
|
}
|
|
2159
2208
|
|
|
2209
|
+
if (isGetLikeRequest && pathname === '/js/buyer-intent.js') {
|
|
2210
|
+
try {
|
|
2211
|
+
const script = fs.readFileSync(BUYER_INTENT_SCRIPT_PATH, 'utf-8');
|
|
2212
|
+
res.writeHead(200, {
|
|
2213
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
2214
|
+
'Cache-Control': 'public, max-age=86400',
|
|
2215
|
+
});
|
|
2216
|
+
if (!isHeadRequest) {
|
|
2217
|
+
res.end(script);
|
|
2218
|
+
} else {
|
|
2219
|
+
res.end();
|
|
2220
|
+
}
|
|
2221
|
+
} catch {
|
|
2222
|
+
sendJson(res, 404, { error: 'Buyer intent script not found' });
|
|
2223
|
+
}
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2160
2227
|
|
|
2161
2228
|
// User feedback → GitHub Issues
|
|
2162
2229
|
if (req.method === 'POST' && pathname === '/api/feedback/submit') {
|
|
@@ -2189,6 +2256,9 @@ function createApiServer() {
|
|
|
2189
2256
|
req.on('data', (chunk) => chunks.push(chunk));
|
|
2190
2257
|
req.on('end', () => {
|
|
2191
2258
|
try {
|
|
2259
|
+
const accepts = String(req.headers.accept || '').toLowerCase();
|
|
2260
|
+
const requestedWith = String(req.headers['x-requested-with'] || '').toLowerCase();
|
|
2261
|
+
const wantsJson = accepts.includes('application/json') || requestedWith === 'fetch';
|
|
2192
2262
|
const body = Buffer.concat(chunks).toString();
|
|
2193
2263
|
const params = new URLSearchParams(body);
|
|
2194
2264
|
const email = (params.get('email') || '').trim().toLowerCase();
|
|
@@ -2199,7 +2269,65 @@ function createApiServer() {
|
|
|
2199
2269
|
const newsletterPath = path.join(getFeedbackPaths().FEEDBACK_DIR, 'newsletter-subscribers.jsonl');
|
|
2200
2270
|
const dir = path.dirname(newsletterPath);
|
|
2201
2271
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
2202
|
-
|
|
2272
|
+
const existingEntries = fs.existsSync(newsletterPath)
|
|
2273
|
+
? fs.readFileSync(newsletterPath, 'utf8').split('\n').map((line) => line.trim()).filter(Boolean)
|
|
2274
|
+
: [];
|
|
2275
|
+
const duplicate = existingEntries.some((line) => {
|
|
2276
|
+
try {
|
|
2277
|
+
const entry = JSON.parse(line);
|
|
2278
|
+
return String(entry.email || '').trim().toLowerCase() === email;
|
|
2279
|
+
} catch {
|
|
2280
|
+
return false;
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
const referrer = String(req.headers.referer || req.headers.referrer || '').trim();
|
|
2284
|
+
let attribution = {};
|
|
2285
|
+
let referrerHost = null;
|
|
2286
|
+
let landingPath = '/';
|
|
2287
|
+
if (referrer) {
|
|
2288
|
+
try {
|
|
2289
|
+
const referrerUrl = new URL(referrer);
|
|
2290
|
+
referrerHost = referrerUrl.host || null;
|
|
2291
|
+
landingPath = referrerUrl.pathname || '/';
|
|
2292
|
+
attribution = {
|
|
2293
|
+
source: referrerUrl.searchParams.get('utm_source') || null,
|
|
2294
|
+
medium: referrerUrl.searchParams.get('utm_medium') || null,
|
|
2295
|
+
campaign: referrerUrl.searchParams.get('utm_campaign') || null,
|
|
2296
|
+
content: referrerUrl.searchParams.get('utm_content') || null,
|
|
2297
|
+
term: referrerUrl.searchParams.get('utm_term') || null,
|
|
2298
|
+
creator: referrerUrl.searchParams.get('creator') || null,
|
|
2299
|
+
community: referrerUrl.searchParams.get('community') || referrerUrl.searchParams.get('subreddit') || null,
|
|
2300
|
+
postId: referrerUrl.searchParams.get('post_id') || referrerUrl.searchParams.get('postId') || null,
|
|
2301
|
+
commentId: referrerUrl.searchParams.get('comment_id') || referrerUrl.searchParams.get('commentId') || null,
|
|
2302
|
+
campaignVariant: referrerUrl.searchParams.get('campaign_variant') || referrerUrl.searchParams.get('variant') || null,
|
|
2303
|
+
offerCode: referrerUrl.searchParams.get('offer_code') || referrerUrl.searchParams.get('offer') || null,
|
|
2304
|
+
landingPath,
|
|
2305
|
+
};
|
|
2306
|
+
} catch {
|
|
2307
|
+
// Ignore invalid referrer values.
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
if (!duplicate) {
|
|
2311
|
+
fs.appendFileSync(newsletterPath, JSON.stringify({
|
|
2312
|
+
email,
|
|
2313
|
+
subscribedAt: new Date().toISOString(),
|
|
2314
|
+
source: attribution.source || 'landing-page',
|
|
2315
|
+
referrer: referrer || null,
|
|
2316
|
+
referrerHost,
|
|
2317
|
+
landingPath,
|
|
2318
|
+
attribution,
|
|
2319
|
+
}) + '\n');
|
|
2320
|
+
}
|
|
2321
|
+
if (wantsJson) {
|
|
2322
|
+
sendJson(res, 200, {
|
|
2323
|
+
accepted: true,
|
|
2324
|
+
duplicate,
|
|
2325
|
+
email,
|
|
2326
|
+
landingPath,
|
|
2327
|
+
source: attribution.source || 'landing-page',
|
|
2328
|
+
});
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2203
2331
|
res.writeHead(302, { Location: '/?subscribed=1' });
|
|
2204
2332
|
res.end();
|
|
2205
2333
|
} catch {
|
|
@@ -2325,7 +2453,7 @@ async function addContext(){
|
|
|
2325
2453
|
const ctx=document.getElementById('contextInput').value.trim();
|
|
2326
2454
|
if(!ctx)return;
|
|
2327
2455
|
try{
|
|
2328
|
-
await fetch('/feedback/quick/context',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({signal:'${signal}',context:ctx,relatedFeedbackId:'${feedbackId}'})});
|
|
2456
|
+
await fetch('/feedback/quick/context',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({signal:'${signal}',context:ctx,relatedFeedbackId:'${feedbackId}',feedbackSessionId:'${result.feedbackSession?.sessionId || ''}'})});
|
|
2329
2457
|
document.getElementById('toast').style.display='block';
|
|
2330
2458
|
document.getElementById('contextForm').style.display='none';
|
|
2331
2459
|
setTimeout(()=>document.getElementById('toast').style.display='none',3000);
|
|
@@ -2343,6 +2471,7 @@ async function addContext(){
|
|
|
2343
2471
|
const signal = body.signal;
|
|
2344
2472
|
const context = typeof body.context === 'string' ? body.context.trim() : '';
|
|
2345
2473
|
const relatedFeedbackId = typeof body.relatedFeedbackId === 'string' ? body.relatedFeedbackId.trim() : '';
|
|
2474
|
+
const feedbackSessionId = typeof body.feedbackSessionId === 'string' ? body.feedbackSessionId.trim() : '';
|
|
2346
2475
|
if (signal !== 'up' && signal !== 'down') {
|
|
2347
2476
|
sendJson(res, 400, { error: 'signal must be up or down' });
|
|
2348
2477
|
return;
|
|
@@ -2351,18 +2480,45 @@ async function addContext(){
|
|
|
2351
2480
|
sendJson(res, 400, { error: 'context is required' });
|
|
2352
2481
|
return;
|
|
2353
2482
|
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2483
|
+
if (!relatedFeedbackId) {
|
|
2484
|
+
sendJson(res, 400, { error: 'relatedFeedbackId is required' });
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
const feedbackDir = getSafeDataDir();
|
|
2488
|
+
const detailField = signal === 'down' ? 'whatWentWrong' : 'whatWorked';
|
|
2489
|
+
const updated = updateLessonRecord(feedbackDir, relatedFeedbackId, (existing) => {
|
|
2490
|
+
const nextTags = Array.from(new Set([
|
|
2491
|
+
...((Array.isArray(existing.tags) ? existing.tags : []).filter(Boolean)),
|
|
2492
|
+
'statusline',
|
|
2493
|
+
'quick-capture',
|
|
2494
|
+
'follow-up-context',
|
|
2495
|
+
]));
|
|
2496
|
+
return {
|
|
2497
|
+
...existing,
|
|
2498
|
+
tags: nextTags,
|
|
2499
|
+
[detailField]: mergeFollowUpDetail(existing[detailField], context),
|
|
2500
|
+
};
|
|
2501
|
+
});
|
|
2502
|
+
if (!updated) {
|
|
2503
|
+
sendJson(res, 404, { error: 'Related lesson not found' });
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
let feedbackSession = null;
|
|
2507
|
+
if (feedbackSessionId) {
|
|
2508
|
+
try {
|
|
2509
|
+
const { appendToSession } = require('../../scripts/feedback-session');
|
|
2510
|
+
feedbackSession = appendToSession(feedbackSessionId, context, 'user');
|
|
2511
|
+
} catch (_err) {
|
|
2512
|
+
feedbackSession = { status: 'error' };
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
sendJson(res, 200, {
|
|
2516
|
+
ok: true,
|
|
2357
2517
|
relatedFeedbackId,
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
}),
|
|
2362
|
-
tags: ['statusline', 'quick-capture', 'follow-up-context'],
|
|
2518
|
+
detailField,
|
|
2519
|
+
updated,
|
|
2520
|
+
feedbackSession,
|
|
2363
2521
|
});
|
|
2364
|
-
const code = result.accepted ? 200 : 422;
|
|
2365
|
-
sendJson(res, code, result);
|
|
2366
2522
|
return;
|
|
2367
2523
|
}
|
|
2368
2524
|
|
|
@@ -2382,28 +2538,27 @@ async function addContext(){
|
|
|
2382
2538
|
const lessonId = decodeURIComponent(lessonUpdateMatch[1]);
|
|
2383
2539
|
const feedbackDir = getSafeDataDir();
|
|
2384
2540
|
const body = await parseJsonBody(req);
|
|
2385
|
-
const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
|
|
2386
2541
|
const record = findRecordById(lessonId, feedbackDir);
|
|
2387
2542
|
if (!record) {
|
|
2388
2543
|
sendJson(res, 404, { error: 'Record not found' });
|
|
2389
2544
|
return;
|
|
2390
2545
|
}
|
|
2391
|
-
const
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2546
|
+
const updated = updateLessonRecord(feedbackDir, lessonId, (existing) => {
|
|
2547
|
+
const next = { ...existing };
|
|
2548
|
+
if (body.title !== undefined) next.title = body.title;
|
|
2549
|
+
if (body.content !== undefined) next.context = body.content;
|
|
2550
|
+
if (body.tags !== undefined) {
|
|
2551
|
+
next.tags = typeof body.tags === 'string'
|
|
2552
|
+
? body.tags.split(',').map((t) => t.trim()).filter(Boolean)
|
|
2553
|
+
: body.tags;
|
|
2554
|
+
}
|
|
2555
|
+
if (body.whatWentWrong !== undefined) next.whatWentWrong = body.whatWentWrong;
|
|
2556
|
+
if (body.whatWorked !== undefined) next.whatWorked = body.whatWorked;
|
|
2557
|
+
return next;
|
|
2558
|
+
});
|
|
2559
|
+
if (!updated) {
|
|
2560
|
+
sendJson(res, 404, { error: 'Record not found' });
|
|
2561
|
+
return;
|
|
2407
2562
|
}
|
|
2408
2563
|
sendJson(res, 200, { ok: true, updated });
|
|
2409
2564
|
return;
|
|
@@ -2472,6 +2627,25 @@ async function addContext(){
|
|
|
2472
2627
|
return;
|
|
2473
2628
|
}
|
|
2474
2629
|
|
|
2630
|
+
if (isGetLikeRequest && pathname === '/pro') {
|
|
2631
|
+
try {
|
|
2632
|
+
servePublicMarketingPage({
|
|
2633
|
+
req,
|
|
2634
|
+
res,
|
|
2635
|
+
parsed,
|
|
2636
|
+
hostedConfig,
|
|
2637
|
+
isHeadRequest,
|
|
2638
|
+
renderHtml: loadProPageHtml,
|
|
2639
|
+
extraTelemetry: {
|
|
2640
|
+
pageType: 'pro_landing',
|
|
2641
|
+
},
|
|
2642
|
+
});
|
|
2643
|
+
} catch (err) {
|
|
2644
|
+
sendText(res, 500, err.message || 'Pro page unavailable');
|
|
2645
|
+
}
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2475
2649
|
if (isGetLikeRequest && pathname === '/guide') {
|
|
2476
2650
|
try {
|
|
2477
2651
|
const html = fs.readFileSync(GUIDE_PAGE_PATH, 'utf-8');
|
|
@@ -2539,7 +2713,7 @@ async function addContext(){
|
|
|
2539
2713
|
version: pkg.version,
|
|
2540
2714
|
status: 'ok',
|
|
2541
2715
|
docs: 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
2542
|
-
endpoints: ['/health', '/dashboard', '/guide', '/learn', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/settings/status', '/v1/dpo/export', '/v1/analytics/databricks/export'],
|
|
2716
|
+
endpoints: ['/health', '/dashboard', '/guide', '/learn', '/pro', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/settings/status', '/v1/dpo/export', '/v1/analytics/databricks/export'],
|
|
2543
2717
|
}, {}, {
|
|
2544
2718
|
headOnly: isHeadRequest,
|
|
2545
2719
|
});
|
|
@@ -3490,6 +3664,80 @@ async function addContext(){
|
|
|
3490
3664
|
return;
|
|
3491
3665
|
}
|
|
3492
3666
|
|
|
3667
|
+
if (req.method === 'POST' && pathname === '/v1/gates/task-scope') {
|
|
3668
|
+
const body = await parseJsonBody(req);
|
|
3669
|
+
const scope = setTaskScope({
|
|
3670
|
+
taskId: body.taskId,
|
|
3671
|
+
summary: body.summary,
|
|
3672
|
+
allowedPaths: body.allowedPaths,
|
|
3673
|
+
protectedPaths: body.protectedPaths,
|
|
3674
|
+
repoPath: body.repoPath,
|
|
3675
|
+
localOnly: body.localOnly === true,
|
|
3676
|
+
clear: body.clear === true,
|
|
3677
|
+
});
|
|
3678
|
+
sendJson(res, 200, { scope });
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
if (req.method === 'GET' && pathname === '/v1/gates/task-scope') {
|
|
3683
|
+
sendJson(res, 200, getScopeState());
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
if (req.method === 'POST' && pathname === '/v1/gates/branch-governance') {
|
|
3688
|
+
const body = await parseJsonBody(req);
|
|
3689
|
+
const branchGovernance = setBranchGovernance({
|
|
3690
|
+
branchName: body.branchName,
|
|
3691
|
+
baseBranch: body.baseBranch,
|
|
3692
|
+
prRequired: body.prRequired,
|
|
3693
|
+
prNumber: body.prNumber,
|
|
3694
|
+
prUrl: body.prUrl,
|
|
3695
|
+
queueRequired: body.queueRequired,
|
|
3696
|
+
localOnly: body.localOnly === true,
|
|
3697
|
+
releaseVersion: body.releaseVersion,
|
|
3698
|
+
releaseEvidence: body.releaseEvidence,
|
|
3699
|
+
releaseSensitiveGlobs: body.releaseSensitiveGlobs,
|
|
3700
|
+
clear: body.clear === true,
|
|
3701
|
+
});
|
|
3702
|
+
sendJson(res, 200, { branchGovernance });
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
if (req.method === 'GET' && pathname === '/v1/gates/branch-governance') {
|
|
3707
|
+
sendJson(res, 200, { branchGovernance: getBranchGovernanceState() });
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
if (req.method === 'POST' && pathname === '/v1/gates/protected-approval') {
|
|
3712
|
+
const body = await parseJsonBody(req);
|
|
3713
|
+
const approval = approveProtectedAction({
|
|
3714
|
+
pathGlobs: body.pathGlobs,
|
|
3715
|
+
reason: body.reason,
|
|
3716
|
+
evidence: body.evidence,
|
|
3717
|
+
taskId: body.taskId,
|
|
3718
|
+
ttlMs: body.ttlMs,
|
|
3719
|
+
});
|
|
3720
|
+
sendJson(res, 200, { approved: true, approval });
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
if (req.method === 'GET' && pathname === '/v1/ops/integrity') {
|
|
3725
|
+
const command = parsed.searchParams.get('command') || undefined;
|
|
3726
|
+
const baseBranch = parsed.searchParams.get('baseBranch') || undefined;
|
|
3727
|
+
const requirePrForReleaseSensitive = parsed.searchParams.get('requirePrForReleaseSensitive') === 'true';
|
|
3728
|
+
const requireVersionNotBehindBase = parsed.searchParams.get('requireVersionNotBehindBase') === 'true';
|
|
3729
|
+
const report = evaluateOperationalIntegrity({
|
|
3730
|
+
repoPath: process.cwd(),
|
|
3731
|
+
baseBranch,
|
|
3732
|
+
command,
|
|
3733
|
+
requirePrForReleaseSensitive,
|
|
3734
|
+
requireVersionNotBehindBase,
|
|
3735
|
+
branchGovernance: getBranchGovernanceState(),
|
|
3736
|
+
});
|
|
3737
|
+
sendJson(res, 200, report);
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3493
3741
|
if (req.method === 'GET' && pathname === '/v1/feedback/summary') {
|
|
3494
3742
|
const recent = Number(parsed.searchParams.get('recent') || 20);
|
|
3495
3743
|
const summary = feedbackSummary(Number.isFinite(recent) ? recent : 20);
|
|
@@ -3521,7 +3769,7 @@ async function addContext(){
|
|
|
3521
3769
|
const signal = parsed.searchParams.get('signal') || null;
|
|
3522
3770
|
let results;
|
|
3523
3771
|
try {
|
|
3524
|
-
results =
|
|
3772
|
+
results = searchThumbgate({
|
|
3525
3773
|
query,
|
|
3526
3774
|
limit: Number.isFinite(limit) ? limit : 10,
|
|
3527
3775
|
source,
|
|
@@ -3538,7 +3786,7 @@ async function addContext(){
|
|
|
3538
3786
|
const body = await parseJsonBody(req);
|
|
3539
3787
|
let results;
|
|
3540
3788
|
try {
|
|
3541
|
-
results =
|
|
3789
|
+
results = searchThumbgate({
|
|
3542
3790
|
query: body.query || body.q || '',
|
|
3543
3791
|
limit: body.limit,
|
|
3544
3792
|
source: body.source,
|
|
@@ -4173,17 +4421,19 @@ async function addContext(){
|
|
|
4173
4421
|
});
|
|
4174
4422
|
}
|
|
4175
4423
|
|
|
4176
|
-
function startServer({ port } = {}) {
|
|
4424
|
+
function startServer({ port, host } = {}) {
|
|
4177
4425
|
const listenPort = Number(port ?? process.env.PORT ?? 8787);
|
|
4426
|
+
const listenHost = String(host ?? process.env.HOST ?? '0.0.0.0').trim() || '0.0.0.0';
|
|
4178
4427
|
const server = createApiServer();
|
|
4179
4428
|
return new Promise((resolve) => {
|
|
4180
|
-
server.listen(listenPort, () => {
|
|
4429
|
+
server.listen(listenPort, listenHost, () => {
|
|
4181
4430
|
const address = server.address();
|
|
4182
4431
|
const actualPort = (address && typeof address === 'object' && address.port)
|
|
4183
4432
|
? address.port
|
|
4184
4433
|
: listenPort;
|
|
4185
4434
|
resolve({
|
|
4186
4435
|
server,
|
|
4436
|
+
host: listenHost,
|
|
4187
4437
|
port: actualPort,
|
|
4188
4438
|
});
|
|
4189
4439
|
});
|
|
@@ -4200,7 +4450,7 @@ module.exports = {
|
|
|
4200
4450
|
};
|
|
4201
4451
|
|
|
4202
4452
|
if (require.main === module) {
|
|
4203
|
-
startServer().then(({ port }) => {
|
|
4204
|
-
console.log(`ThumbGate API listening on http
|
|
4453
|
+
startServer().then(({ host, port }) => {
|
|
4454
|
+
console.log(`ThumbGate API listening on http://${host}:${port}`);
|
|
4205
4455
|
});
|
|
4206
4456
|
}
|
|
Binary file
|
|
Binary file
|