thumbgate 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +32 -13
- package/.claude-plugin/plugin.json +15 -2
- package/.well-known/llms.txt +60 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +109 -20
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +84 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +200 -13
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +42 -10
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +10 -2
- package/public/guide.html +2 -2
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +136 -50
- package/public/lessons.html +33 -24
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +1 -5
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/check-congruence.js +132 -14
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +1 -21
- package/scripts/dashboard.js +20 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +26 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +1 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +188 -28
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +149 -4
- package/scripts/github-about.js +32 -8
- package/scripts/gtm-revenue-loop.js +1 -5
- package/scripts/harness-selector.js +148 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +7 -33
- package/scripts/intervention-policy.js +58 -1
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +15 -3
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +1 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +1 -3
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +75 -9
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/rate-limiter.js +3 -1
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline.sh +29 -153
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +106 -2
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +91 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +273 -4
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
|
@@ -705,6 +705,11 @@ function buildReasoning(report) {
|
|
|
705
705
|
`Workflow sentinel risk ${report.band} (${report.riskScore}) for ${report.toolName}.`,
|
|
706
706
|
`Blast radius: ${report.blastRadius.summary}.`,
|
|
707
707
|
];
|
|
708
|
+
if (report.decisionControl) {
|
|
709
|
+
lines.push(
|
|
710
|
+
`Decision control: ${report.decisionControl.decisionOwner} owns a ${report.decisionControl.reversibility} action via ${report.decisionControl.executionMode}.`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
708
713
|
if (report.learnedPolicy && report.learnedPolicy.enabled && report.learnedPolicy.prediction) {
|
|
709
714
|
lines.push(
|
|
710
715
|
`Learned policy predicted ${report.learnedPolicy.prediction.label} (${report.learnedPolicy.prediction.confidence}).`
|
|
@@ -732,6 +737,80 @@ function getSentinelActionType(toolName) {
|
|
|
732
737
|
return '';
|
|
733
738
|
}
|
|
734
739
|
|
|
740
|
+
function classifyReversibility({ command, blastRadius, integrity, protectedSurface }) {
|
|
741
|
+
const text = String(command || '');
|
|
742
|
+
const blockers = integrity && Array.isArray(integrity.blockers) ? integrity.blockers : [];
|
|
743
|
+
const destructiveCommand = /\bgit\s+push\b.*(?:--force|-f)\b/i.test(text)
|
|
744
|
+
|| /\bgh\s+pr\s+merge\b.*--admin\b/i.test(text)
|
|
745
|
+
|| /\brm\s+-rf\b/i.test(text)
|
|
746
|
+
|| /\b(?:npm|yarn|pnpm)\s+publish\b/i.test(text)
|
|
747
|
+
|| /\bgh\s+release\s+create\b/i.test(text)
|
|
748
|
+
|| /\bgit\s+tag\b/i.test(text);
|
|
749
|
+
const releaseSensitive = blastRadius && Array.isArray(blastRadius.releaseSensitiveFiles)
|
|
750
|
+
? blastRadius.releaseSensitiveFiles.length > 0
|
|
751
|
+
: false;
|
|
752
|
+
const unapprovedProtected = protectedSurface && Array.isArray(protectedSurface.unapprovedProtectedFiles)
|
|
753
|
+
? protectedSurface.unapprovedProtectedFiles.length > 0
|
|
754
|
+
: false;
|
|
755
|
+
const hardBlockers = blockers.some((blocker) => /publish|merge|release|protected/i.test(String(blocker.code || '')));
|
|
756
|
+
|
|
757
|
+
if (destructiveCommand || releaseSensitive || unapprovedProtected || hardBlockers) {
|
|
758
|
+
return 'one_way_door';
|
|
759
|
+
}
|
|
760
|
+
if ((blastRadius && blastRadius.fileCount >= 4) || (blastRadius && blastRadius.surfaceCount >= 2)) {
|
|
761
|
+
return 'reviewable';
|
|
762
|
+
}
|
|
763
|
+
return 'two_way_door';
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function buildDecisionControl({
|
|
767
|
+
decision,
|
|
768
|
+
risk,
|
|
769
|
+
command,
|
|
770
|
+
blastRadius,
|
|
771
|
+
integrity,
|
|
772
|
+
protectedSurface,
|
|
773
|
+
}) {
|
|
774
|
+
const reversibility = classifyReversibility({
|
|
775
|
+
command,
|
|
776
|
+
blastRadius,
|
|
777
|
+
integrity,
|
|
778
|
+
protectedSurface,
|
|
779
|
+
});
|
|
780
|
+
const hasOperationalBlockers = Boolean(integrity && Array.isArray(integrity.blockers) && integrity.blockers.length > 0);
|
|
781
|
+
const requiresCheckpoint = decision === 'warn'
|
|
782
|
+
|| (decision === 'allow' && (reversibility !== 'two_way_door' || hasOperationalBlockers));
|
|
783
|
+
const executionMode = decision === 'deny'
|
|
784
|
+
? 'blocked'
|
|
785
|
+
: requiresCheckpoint
|
|
786
|
+
? 'checkpoint_required'
|
|
787
|
+
: 'auto_execute';
|
|
788
|
+
const decisionOwner = executionMode === 'blocked'
|
|
789
|
+
? 'human'
|
|
790
|
+
: executionMode === 'checkpoint_required'
|
|
791
|
+
? reversibility === 'two_way_door' && !hasOperationalBlockers
|
|
792
|
+
? 'shared'
|
|
793
|
+
: 'human'
|
|
794
|
+
: 'agent';
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
executionMode,
|
|
798
|
+
decisionOwner,
|
|
799
|
+
reversibility,
|
|
800
|
+
requiresHumanApproval: executionMode === 'checkpoint_required' && decisionOwner !== 'agent',
|
|
801
|
+
recommendedAction: executionMode === 'blocked'
|
|
802
|
+
? 'halt'
|
|
803
|
+
: executionMode === 'checkpoint_required'
|
|
804
|
+
? 'review'
|
|
805
|
+
: 'proceed',
|
|
806
|
+
summary: executionMode === 'blocked'
|
|
807
|
+
? 'Do not proceed until the remediation steps are completed.'
|
|
808
|
+
: executionMode === 'checkpoint_required'
|
|
809
|
+
? 'Pause for explicit review before executing this action.'
|
|
810
|
+
: 'Safe to execute quickly with standard evidence capture.',
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
735
814
|
function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blastRadius, command }) {
|
|
736
815
|
const hasOperationalBlockers = Boolean(integrity && Array.isArray(integrity.blockers) && integrity.blockers.length > 0);
|
|
737
816
|
const destructiveBypass = /\bgit\s+push\b.*(?:--force|-f)\b/i.test(command) || /\bgh\s+pr\s+merge\b.*--admin\b/i.test(command);
|
|
@@ -923,11 +1002,23 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
|
|
|
923
1002
|
commandInfo: integrity.commandInfo,
|
|
924
1003
|
},
|
|
925
1004
|
};
|
|
1005
|
+
report.decisionControl = buildDecisionControl({
|
|
1006
|
+
decision,
|
|
1007
|
+
risk,
|
|
1008
|
+
command: toolInput.command || '',
|
|
1009
|
+
blastRadius: {
|
|
1010
|
+
...blastRadius,
|
|
1011
|
+
unapprovedProtectedFiles: protectedSurfaceForRisk.unapprovedProtectedFiles.length,
|
|
1012
|
+
},
|
|
1013
|
+
integrity,
|
|
1014
|
+
protectedSurface: protectedSurfaceForRisk,
|
|
1015
|
+
});
|
|
926
1016
|
report.reasoning = buildReasoning(report);
|
|
927
1017
|
return report;
|
|
928
1018
|
}
|
|
929
1019
|
|
|
930
1020
|
module.exports = {
|
|
1021
|
+
buildDecisionControl,
|
|
931
1022
|
DEFAULT_PROTECTED_FILE_GLOBS,
|
|
932
1023
|
buildBlastRadius,
|
|
933
1024
|
buildEvidence,
|
|
@@ -92,7 +92,7 @@ Bounded retrieval of relevant feedback history for the current task. The agent g
|
|
|
92
92
|
| Dashboard | - | Yes | Yes |
|
|
93
93
|
| DPO export | - | Yes | Yes |
|
|
94
94
|
| Seats | 1 | 1 | Per-seat |
|
|
95
|
-
| Price | $0 | $19/mo | $
|
|
95
|
+
| Price | $0 | $19/mo | $99/seat/mo |
|
|
96
96
|
|
|
97
97
|
Start a 7-day free trial of Pro: <https://buy.stripe.com/fZu9AT3Ug6zcdWh0XN3sI08>
|
|
98
98
|
|
package/src/api/server.js
CHANGED
|
@@ -108,6 +108,14 @@ const {
|
|
|
108
108
|
const {
|
|
109
109
|
evaluateOperationalIntegrity,
|
|
110
110
|
} = require('../../scripts/operational-integrity');
|
|
111
|
+
const {
|
|
112
|
+
evaluateWorkflowSentinel,
|
|
113
|
+
} = require('../../scripts/workflow-sentinel');
|
|
114
|
+
const {
|
|
115
|
+
recordDecisionEvaluation,
|
|
116
|
+
recordDecisionOutcome,
|
|
117
|
+
computeDecisionMetrics,
|
|
118
|
+
} = require('../../scripts/decision-journal');
|
|
111
119
|
const {
|
|
112
120
|
generateDashboard,
|
|
113
121
|
} = require('../../scripts/dashboard');
|
|
@@ -152,6 +160,11 @@ const {
|
|
|
152
160
|
appendWorkflowSprintLead,
|
|
153
161
|
advanceWorkflowSprintLead,
|
|
154
162
|
} = require('../../scripts/workflow-sprint-intake');
|
|
163
|
+
const {
|
|
164
|
+
importDocument,
|
|
165
|
+
listImportedDocuments,
|
|
166
|
+
readImportedDocument,
|
|
167
|
+
} = require('../../scripts/document-intake');
|
|
155
168
|
const {
|
|
156
169
|
checkLimit,
|
|
157
170
|
UPGRADE_MESSAGE: RATE_LIMIT_MESSAGE,
|
|
@@ -172,6 +185,8 @@ const GUIDE_PAGE_PATH = path.resolve(__dirname, '../../public/guide.html');
|
|
|
172
185
|
const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
|
|
173
186
|
const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
|
|
174
187
|
const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
|
|
188
|
+
const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
|
|
189
|
+
const COMPARE_DIR = path.resolve(__dirname, '../../public/compare');
|
|
175
190
|
const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
|
|
176
191
|
const VISITOR_COOKIE_NAME = 'thumbgate_visitor_id';
|
|
177
192
|
const SESSION_COOKIE_NAME = 'thumbgate_session_id';
|
|
@@ -1345,6 +1360,32 @@ function renderRobotsTxt(runtimeConfig) {
|
|
|
1345
1360
|
return [
|
|
1346
1361
|
'User-agent: *',
|
|
1347
1362
|
'Allow: /',
|
|
1363
|
+
'',
|
|
1364
|
+
'# AI crawler access — allow all major LLM crawlers',
|
|
1365
|
+
'User-agent: GPTBot',
|
|
1366
|
+
'Allow: /',
|
|
1367
|
+
'',
|
|
1368
|
+
'User-agent: ClaudeBot',
|
|
1369
|
+
'Allow: /',
|
|
1370
|
+
'',
|
|
1371
|
+
'User-agent: PerplexityBot',
|
|
1372
|
+
'Allow: /',
|
|
1373
|
+
'',
|
|
1374
|
+
'User-agent: Googlebot',
|
|
1375
|
+
'Allow: /',
|
|
1376
|
+
'',
|
|
1377
|
+
'User-agent: Bingbot',
|
|
1378
|
+
'Allow: /',
|
|
1379
|
+
'',
|
|
1380
|
+
'User-agent: anthropic-ai',
|
|
1381
|
+
'Allow: /',
|
|
1382
|
+
'',
|
|
1383
|
+
'User-agent: Google-Extended',
|
|
1384
|
+
'Allow: /',
|
|
1385
|
+
'',
|
|
1386
|
+
'# LLM context document — clean declarative content for AI retrieval',
|
|
1387
|
+
`# ${runtimeConfig.appOrigin}/llm-context.md`,
|
|
1388
|
+
'',
|
|
1348
1389
|
`Sitemap: ${runtimeConfig.appOrigin}/sitemap.xml`,
|
|
1349
1390
|
].join('\n');
|
|
1350
1391
|
}
|
|
@@ -1353,6 +1394,7 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
1353
1394
|
const entries = [
|
|
1354
1395
|
{ path: '/', changefreq: 'weekly', priority: '1.0' },
|
|
1355
1396
|
{ path: '/pro', changefreq: 'weekly', priority: '0.9' },
|
|
1397
|
+
{ path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
|
|
1356
1398
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
1357
1399
|
];
|
|
1358
1400
|
return [
|
|
@@ -2240,6 +2282,40 @@ function normalizeJobIdFromPath(pathname, suffix = '') {
|
|
|
2240
2282
|
return match ? decodeURIComponent(match[1]) : null;
|
|
2241
2283
|
}
|
|
2242
2284
|
|
|
2285
|
+
function normalizeDocumentIdFromPath(pathname) {
|
|
2286
|
+
const match = pathname.match(/^\/v1\/documents\/([^/]+)$/);
|
|
2287
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
function isWithinDir(targetPath, baseDir) {
|
|
2291
|
+
if (!baseDir) return false;
|
|
2292
|
+
const resolvedBase = path.resolve(baseDir);
|
|
2293
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
2294
|
+
const relative = path.relative(resolvedBase, resolvedTarget);
|
|
2295
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
function resolveDocumentImportFilePath(inputPath, options = {}) {
|
|
2299
|
+
const normalized = normalizeNullableText(inputPath);
|
|
2300
|
+
if (!normalized) return null;
|
|
2301
|
+
|
|
2302
|
+
const { req, parsed, safeDataDir } = options;
|
|
2303
|
+
if (!isLoopbackHost(getRequestHostHeader(req))) {
|
|
2304
|
+
throw createHttpError(400, 'filePath import is only available on localhost requests; use content for hosted imports');
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
const projectDir = resolveRequestProjectDir(req, parsed) || process.cwd();
|
|
2308
|
+
const resolved = path.resolve(projectDir, normalized);
|
|
2309
|
+
const allowed = isWithinDir(resolved, projectDir) || isWithinDir(resolved, safeDataDir);
|
|
2310
|
+
if (!allowed) {
|
|
2311
|
+
throw createHttpError(400, `Path must stay within ${projectDir} or ${safeDataDir}`);
|
|
2312
|
+
}
|
|
2313
|
+
if (!fs.existsSync(resolved)) {
|
|
2314
|
+
throw createHttpError(400, `Path does not exist: ${resolved}`);
|
|
2315
|
+
}
|
|
2316
|
+
return resolved;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2243
2319
|
function createApiServer() {
|
|
2244
2320
|
const expectedApiKey = getExpectedApiKey();
|
|
2245
2321
|
|
|
@@ -2516,6 +2592,17 @@ function createApiServer() {
|
|
|
2516
2592
|
return;
|
|
2517
2593
|
}
|
|
2518
2594
|
|
|
2595
|
+
if (isGetLikeRequest && pathname === '/.well-known/llms.txt') {
|
|
2596
|
+
const llmsTxtPath = path.join(__dirname, '..', '..', '.well-known', 'llms.txt');
|
|
2597
|
+
try {
|
|
2598
|
+
const content = fs.readFileSync(llmsTxtPath, 'utf8');
|
|
2599
|
+
sendText(res, 200, content, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=86400' }, { headOnly: isHeadRequest });
|
|
2600
|
+
} catch {
|
|
2601
|
+
sendJson(res, 404, { error: 'llms.txt not found' });
|
|
2602
|
+
}
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2519
2606
|
if (isGetLikeRequest && pathname === '/sitemap.xml') {
|
|
2520
2607
|
sendText(res, 200, renderSitemapXml(hostedConfig), {
|
|
2521
2608
|
'Content-Type': 'application/xml; charset=utf-8',
|
|
@@ -2525,6 +2612,22 @@ function createApiServer() {
|
|
|
2525
2612
|
return;
|
|
2526
2613
|
}
|
|
2527
2614
|
|
|
2615
|
+
if (isGetLikeRequest && pathname === '/llm-context.md') {
|
|
2616
|
+
const llmContextPath = path.resolve(__dirname, '../../public/llm-context.md');
|
|
2617
|
+
try {
|
|
2618
|
+
const content = fs.readFileSync(llmContextPath, 'utf8');
|
|
2619
|
+
sendText(res, 200, content, {
|
|
2620
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
2621
|
+
'X-Robots-Tag': 'all',
|
|
2622
|
+
}, {
|
|
2623
|
+
headOnly: isHeadRequest,
|
|
2624
|
+
});
|
|
2625
|
+
} catch (_err) {
|
|
2626
|
+
sendJson(res, 404, { error: 'Not found' });
|
|
2627
|
+
}
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2528
2631
|
// Quick feedback capture via GET — for statusline clickable links
|
|
2529
2632
|
if (isGetLikeRequest && pathname === '/feedback/quick') {
|
|
2530
2633
|
const signal = parsed.searchParams.get('signal');
|
|
@@ -2852,6 +2955,28 @@ async function addContext(){
|
|
|
2852
2955
|
return;
|
|
2853
2956
|
}
|
|
2854
2957
|
|
|
2958
|
+
if (isGetLikeRequest && pathname.startsWith('/guides/')) {
|
|
2959
|
+
try {
|
|
2960
|
+
const slug = pathname.replace('/guides/', '').replace(/[^a-z0-9-]/g, '');
|
|
2961
|
+
const guidePath = path.join(GUIDES_DIR, `${slug}.html`);
|
|
2962
|
+
if (!guidePath.startsWith(GUIDES_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
|
|
2963
|
+
const html = fs.readFileSync(guidePath, 'utf-8');
|
|
2964
|
+
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
2965
|
+
} catch { sendJson(res, 404, { error: 'Guide not found' }); }
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
if (isGetLikeRequest && pathname.startsWith('/compare/') && pathname !== '/compare') {
|
|
2970
|
+
try {
|
|
2971
|
+
const slug = pathname.replace('/compare/', '').replace(/[^a-z0-9-]/g, '');
|
|
2972
|
+
const comparePath = path.join(COMPARE_DIR, `${slug}.html`);
|
|
2973
|
+
if (!comparePath.startsWith(COMPARE_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
|
|
2974
|
+
const html = fs.readFileSync(comparePath, 'utf-8');
|
|
2975
|
+
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
2976
|
+
} catch { sendJson(res, 404, { error: 'Comparison not found' }); }
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2855
2980
|
if (isGetLikeRequest && pathname === '/') {
|
|
2856
2981
|
if (wantsJson(req, parsed)) {
|
|
2857
2982
|
sendJson(res, 200, {
|
|
@@ -2859,7 +2984,7 @@ async function addContext(){
|
|
|
2859
2984
|
version: pkg.version,
|
|
2860
2985
|
status: 'ok',
|
|
2861
2986
|
docs: 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
2862
|
-
endpoints: ['/health', '/dashboard', '/guide', '/compare', '/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/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
|
|
2987
|
+
endpoints: ['/health', '/dashboard', '/guide', '/compare', '/learn', '/pro', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/documents', '/v1/documents/import', '/v1/documents/{documentId}', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/decisions/evaluate', '/v1/decisions/outcome', '/v1/decisions/metrics', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
|
|
2863
2988
|
}, {}, {
|
|
2864
2989
|
headOnly: isHeadRequest,
|
|
2865
2990
|
});
|
|
@@ -4054,6 +4179,34 @@ async function addContext(){
|
|
|
4054
4179
|
return;
|
|
4055
4180
|
}
|
|
4056
4181
|
|
|
4182
|
+
if (req.method === 'GET' && pathname === '/v1/documents') {
|
|
4183
|
+
const limit = Number(parsed.searchParams.get('limit') || 20);
|
|
4184
|
+
const query = parsed.searchParams.get('q') || parsed.searchParams.get('query') || '';
|
|
4185
|
+
const tag = parsed.searchParams.get('tag') || '';
|
|
4186
|
+
const results = listImportedDocuments({
|
|
4187
|
+
feedbackDir: requestFeedbackDir,
|
|
4188
|
+
limit: Number.isFinite(limit) ? limit : 20,
|
|
4189
|
+
query,
|
|
4190
|
+
tag,
|
|
4191
|
+
});
|
|
4192
|
+
sendJson(res, 200, results);
|
|
4193
|
+
return;
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
{
|
|
4197
|
+
const documentId = normalizeDocumentIdFromPath(pathname);
|
|
4198
|
+
if (req.method === 'GET' && documentId) {
|
|
4199
|
+
const document = readImportedDocument(documentId, {
|
|
4200
|
+
feedbackDir: requestFeedbackDir,
|
|
4201
|
+
});
|
|
4202
|
+
if (!document) {
|
|
4203
|
+
throw createHttpError(404, `Imported document not found: ${documentId}`);
|
|
4204
|
+
}
|
|
4205
|
+
sendJson(res, 200, { document });
|
|
4206
|
+
return;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4057
4210
|
if (req.method === 'POST' && pathname === '/v1/feedback/capture') {
|
|
4058
4211
|
const captureLimit = checkLimit('capture_feedback');
|
|
4059
4212
|
if (!captureLimit.allowed) {
|
|
@@ -4127,6 +4280,31 @@ async function addContext(){
|
|
|
4127
4280
|
return;
|
|
4128
4281
|
}
|
|
4129
4282
|
|
|
4283
|
+
if (req.method === 'POST' && pathname === '/v1/documents/import') {
|
|
4284
|
+
const body = await parseJsonBody(req, 2 * 1024 * 1024);
|
|
4285
|
+
const document = importDocument({
|
|
4286
|
+
filePath: body.filePath
|
|
4287
|
+
? resolveDocumentImportFilePath(body.filePath, {
|
|
4288
|
+
req,
|
|
4289
|
+
parsed,
|
|
4290
|
+
safeDataDir: requestSafeDataDir,
|
|
4291
|
+
})
|
|
4292
|
+
: null,
|
|
4293
|
+
content: normalizeNullableText(body.content),
|
|
4294
|
+
title: normalizeNullableText(body.title),
|
|
4295
|
+
sourceFormat: normalizeNullableText(body.sourceFormat),
|
|
4296
|
+
sourceUrl: normalizeNullableText(body.sourceUrl),
|
|
4297
|
+
tags: extractTags(body.tags),
|
|
4298
|
+
proposeGates: body.proposeGates !== false,
|
|
4299
|
+
feedbackDir: requestFeedbackDir,
|
|
4300
|
+
});
|
|
4301
|
+
sendJson(res, 201, {
|
|
4302
|
+
ok: true,
|
|
4303
|
+
document,
|
|
4304
|
+
});
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4130
4308
|
if (req.method === 'POST' && pathname === '/v1/dpo/export') {
|
|
4131
4309
|
const body = await parseJsonBody(req);
|
|
4132
4310
|
const paths = resolveDpoExportPaths(body, {
|
|
@@ -4569,6 +4747,85 @@ async function addContext(){
|
|
|
4569
4747
|
return;
|
|
4570
4748
|
}
|
|
4571
4749
|
|
|
4750
|
+
if (req.method === 'POST' && pathname === '/v1/decisions/evaluate') {
|
|
4751
|
+
const body = await parseJsonBody(req);
|
|
4752
|
+
if (!body.toolName) {
|
|
4753
|
+
sendProblem(res, {
|
|
4754
|
+
type: PROBLEM_TYPES.BAD_REQUEST,
|
|
4755
|
+
title: 'Bad Request',
|
|
4756
|
+
status: 400,
|
|
4757
|
+
detail: 'toolName is required.',
|
|
4758
|
+
});
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
const report = evaluateWorkflowSentinel(body.toolName, {
|
|
4763
|
+
command: body.command,
|
|
4764
|
+
path: body.filePath,
|
|
4765
|
+
changed_files: Array.isArray(body.changedFiles) ? body.changedFiles : [],
|
|
4766
|
+
repoPath: body.repoPath,
|
|
4767
|
+
baseBranch: body.baseBranch,
|
|
4768
|
+
}, {
|
|
4769
|
+
repoPath: body.repoPath,
|
|
4770
|
+
baseBranch: body.baseBranch,
|
|
4771
|
+
affectedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : undefined,
|
|
4772
|
+
requirePrForReleaseSensitive: body.requirePrForReleaseSensitive === true,
|
|
4773
|
+
requireVersionNotBehindBase: body.requireVersionNotBehindBase === true,
|
|
4774
|
+
governanceState: getScopeState(),
|
|
4775
|
+
feedbackDir: requestFeedbackDir,
|
|
4776
|
+
});
|
|
4777
|
+
const evaluation = recordDecisionEvaluation(report, {
|
|
4778
|
+
source: 'api',
|
|
4779
|
+
toolName: body.toolName,
|
|
4780
|
+
toolInput: {
|
|
4781
|
+
command: body.command,
|
|
4782
|
+
filePath: body.filePath,
|
|
4783
|
+
changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
|
|
4784
|
+
repoPath: body.repoPath,
|
|
4785
|
+
baseBranch: body.baseBranch,
|
|
4786
|
+
},
|
|
4787
|
+
changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
|
|
4788
|
+
}, {
|
|
4789
|
+
feedbackDir: requestFeedbackDir,
|
|
4790
|
+
});
|
|
4791
|
+
report.actionId = evaluation.actionId;
|
|
4792
|
+
if (report.decisionControl) report.decisionControl.actionId = evaluation.actionId;
|
|
4793
|
+
sendJson(res, 200, report);
|
|
4794
|
+
return;
|
|
4795
|
+
}
|
|
4796
|
+
|
|
4797
|
+
if (req.method === 'POST' && pathname === '/v1/decisions/outcome') {
|
|
4798
|
+
const body = await parseJsonBody(req);
|
|
4799
|
+
if (!body.actionId || !body.outcome) {
|
|
4800
|
+
sendProblem(res, {
|
|
4801
|
+
type: PROBLEM_TYPES.BAD_REQUEST,
|
|
4802
|
+
title: 'Bad Request',
|
|
4803
|
+
status: 400,
|
|
4804
|
+
detail: 'actionId and outcome are required.',
|
|
4805
|
+
});
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
const outcome = recordDecisionOutcome({
|
|
4809
|
+
actionId: body.actionId,
|
|
4810
|
+
outcome: body.outcome,
|
|
4811
|
+
actualDecision: body.actualDecision,
|
|
4812
|
+
actor: body.actor,
|
|
4813
|
+
notes: body.notes,
|
|
4814
|
+
metadata: body.metadata,
|
|
4815
|
+
latencyMs: body.latencyMs,
|
|
4816
|
+
source: 'api',
|
|
4817
|
+
}, {
|
|
4818
|
+
feedbackDir: requestFeedbackDir,
|
|
4819
|
+
});
|
|
4820
|
+
sendJson(res, 200, outcome);
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4823
|
+
|
|
4824
|
+
if (req.method === 'GET' && pathname === '/v1/decisions/metrics') {
|
|
4825
|
+
sendJson(res, 200, computeDecisionMetrics(requestFeedbackDir));
|
|
4826
|
+
return;
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4572
4829
|
// GET /v1/settings/status -- Resolved settings hierarchy with origin metadata
|
|
4573
4830
|
if (req.method === 'GET' && pathname === '/v1/settings/status') {
|
|
4574
4831
|
sendJson(res, 200, getSettingsStatus());
|
|
@@ -4599,9 +4856,9 @@ async function addContext(){
|
|
|
4599
4856
|
return;
|
|
4600
4857
|
}
|
|
4601
4858
|
|
|
4602
|
-
// POST /webhook/stripe —
|
|
4603
|
-
//
|
|
4604
|
-
//
|
|
4859
|
+
// POST /webhook/stripe — legacy Stripe event log bridge kept for backward compatibility.
|
|
4860
|
+
// When STRIPE_WEBHOOK_SECRET is configured, verify the same Stripe signature used by
|
|
4861
|
+
// the /v1/billing/webhook route before touching any payload.
|
|
4605
4862
|
if (req.method === 'POST' && pathname === '/webhook/stripe') {
|
|
4606
4863
|
try {
|
|
4607
4864
|
const rawBody = await new Promise((resolve, reject) => {
|
|
@@ -4610,6 +4867,18 @@ async function addContext(){
|
|
|
4610
4867
|
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
4611
4868
|
req.on('error', reject);
|
|
4612
4869
|
});
|
|
4870
|
+
|
|
4871
|
+
const sig = req.headers['stripe-signature'] || '';
|
|
4872
|
+
if (!verifyWebhookSignature(rawBody, sig)) {
|
|
4873
|
+
sendProblem(res, {
|
|
4874
|
+
type: PROBLEM_TYPES.WEBHOOK_INVALID,
|
|
4875
|
+
title: 'Invalid webhook signature',
|
|
4876
|
+
status: 400,
|
|
4877
|
+
detail: 'The webhook signature could not be verified.',
|
|
4878
|
+
});
|
|
4879
|
+
return;
|
|
4880
|
+
}
|
|
4881
|
+
|
|
4613
4882
|
let event;
|
|
4614
4883
|
try {
|
|
4615
4884
|
event = JSON.parse(rawBody.toString('utf-8'));
|
|
Binary file
|
|
File without changes
|