thumbgate 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/README.md +25 -0
- 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 +242 -126
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/INSTALL.md +59 -4
- 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 +204 -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 +47 -11
- 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 +172 -65
- package/public/lessons.html +33 -24
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- 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/auto-promote-gates.js +5 -3
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing-setup.js +109 -0
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-claude-mcpb.js +71 -5
- 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 +54 -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 +215 -36
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +200 -11
- 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-config.js +2 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +33 -49
- 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/operational-summary.js +41 -5
- 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/ralph-loop.js +376 -0
- package/scripts/ralph-mode-ci.js +331 -0
- package/scripts/rate-limiter.js +3 -1
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/rotate-stripe-webhook-secret.js +314 -0
- 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 +296 -7
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- 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';
|
|
@@ -1005,6 +1020,7 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
|
|
|
1005
1020
|
'__AUTOMATION_REPORT_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/proof/automation/report.json',
|
|
1006
1021
|
'__GTM_PLAN_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/GO_TO_MARKET_REVENUE_WEDGE_2026-03.md',
|
|
1007
1022
|
'__GITHUB_URL__': 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
1023
|
+
'__POSTHOG_API_KEY__': runtimeConfig.posthogApiKey || '',
|
|
1008
1024
|
});
|
|
1009
1025
|
}
|
|
1010
1026
|
|
|
@@ -1345,6 +1361,32 @@ function renderRobotsTxt(runtimeConfig) {
|
|
|
1345
1361
|
return [
|
|
1346
1362
|
'User-agent: *',
|
|
1347
1363
|
'Allow: /',
|
|
1364
|
+
'',
|
|
1365
|
+
'# AI crawler access — allow all major LLM crawlers',
|
|
1366
|
+
'User-agent: GPTBot',
|
|
1367
|
+
'Allow: /',
|
|
1368
|
+
'',
|
|
1369
|
+
'User-agent: ClaudeBot',
|
|
1370
|
+
'Allow: /',
|
|
1371
|
+
'',
|
|
1372
|
+
'User-agent: PerplexityBot',
|
|
1373
|
+
'Allow: /',
|
|
1374
|
+
'',
|
|
1375
|
+
'User-agent: Googlebot',
|
|
1376
|
+
'Allow: /',
|
|
1377
|
+
'',
|
|
1378
|
+
'User-agent: Bingbot',
|
|
1379
|
+
'Allow: /',
|
|
1380
|
+
'',
|
|
1381
|
+
'User-agent: anthropic-ai',
|
|
1382
|
+
'Allow: /',
|
|
1383
|
+
'',
|
|
1384
|
+
'User-agent: Google-Extended',
|
|
1385
|
+
'Allow: /',
|
|
1386
|
+
'',
|
|
1387
|
+
'# LLM context document — clean declarative content for AI retrieval',
|
|
1388
|
+
`# ${runtimeConfig.appOrigin}/llm-context.md`,
|
|
1389
|
+
'',
|
|
1348
1390
|
`Sitemap: ${runtimeConfig.appOrigin}/sitemap.xml`,
|
|
1349
1391
|
].join('\n');
|
|
1350
1392
|
}
|
|
@@ -1353,6 +1395,7 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
1353
1395
|
const entries = [
|
|
1354
1396
|
{ path: '/', changefreq: 'weekly', priority: '1.0' },
|
|
1355
1397
|
{ path: '/pro', changefreq: 'weekly', priority: '0.9' },
|
|
1398
|
+
{ path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
|
|
1356
1399
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
1357
1400
|
];
|
|
1358
1401
|
return [
|
|
@@ -2111,6 +2154,11 @@ function getExpectedApiKey() {
|
|
|
2111
2154
|
return configured;
|
|
2112
2155
|
}
|
|
2113
2156
|
|
|
2157
|
+
function getExpectedOperatorKey() {
|
|
2158
|
+
const key = String(process.env.THUMBGATE_OPERATOR_KEY || '').trim();
|
|
2159
|
+
return key || null;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2114
2162
|
function isAuthorized(req, expected) {
|
|
2115
2163
|
if (!expected) return true;
|
|
2116
2164
|
const token = extractApiKey(req);
|
|
@@ -2162,6 +2210,19 @@ function isStaticAdminAuthorized(req, expected) {
|
|
|
2162
2210
|
return extractApiKey(req) === expected;
|
|
2163
2211
|
}
|
|
2164
2212
|
|
|
2213
|
+
/**
|
|
2214
|
+
* Billing summary guard: accepts either the static admin key OR the operator key.
|
|
2215
|
+
* The operator key (THUMBGATE_OPERATOR_KEY) allows read-only billing data access
|
|
2216
|
+
* without exposing the full admin key to CLI clients.
|
|
2217
|
+
*/
|
|
2218
|
+
function isBillingSummaryAuthorized(req, expectedAdminKey, expectedOperatorKey) {
|
|
2219
|
+
if (!expectedAdminKey && !expectedOperatorKey) return true;
|
|
2220
|
+
const token = extractApiKey(req);
|
|
2221
|
+
if (expectedAdminKey && token === expectedAdminKey) return true;
|
|
2222
|
+
if (expectedOperatorKey && token === expectedOperatorKey) return true;
|
|
2223
|
+
return false;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2165
2226
|
function extractTags(input) {
|
|
2166
2227
|
if (Array.isArray(input)) return input;
|
|
2167
2228
|
if (typeof input === 'string') {
|
|
@@ -2240,8 +2301,43 @@ function normalizeJobIdFromPath(pathname, suffix = '') {
|
|
|
2240
2301
|
return match ? decodeURIComponent(match[1]) : null;
|
|
2241
2302
|
}
|
|
2242
2303
|
|
|
2304
|
+
function normalizeDocumentIdFromPath(pathname) {
|
|
2305
|
+
const match = pathname.match(/^\/v1\/documents\/([^/]+)$/);
|
|
2306
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
function isWithinDir(targetPath, baseDir) {
|
|
2310
|
+
if (!baseDir) return false;
|
|
2311
|
+
const resolvedBase = path.resolve(baseDir);
|
|
2312
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
2313
|
+
const relative = path.relative(resolvedBase, resolvedTarget);
|
|
2314
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
function resolveDocumentImportFilePath(inputPath, options = {}) {
|
|
2318
|
+
const normalized = normalizeNullableText(inputPath);
|
|
2319
|
+
if (!normalized) return null;
|
|
2320
|
+
|
|
2321
|
+
const { req, parsed, safeDataDir } = options;
|
|
2322
|
+
if (!isLoopbackHost(getRequestHostHeader(req))) {
|
|
2323
|
+
throw createHttpError(400, 'filePath import is only available on localhost requests; use content for hosted imports');
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
const projectDir = resolveRequestProjectDir(req, parsed) || process.cwd();
|
|
2327
|
+
const resolved = path.resolve(projectDir, normalized);
|
|
2328
|
+
const allowed = isWithinDir(resolved, projectDir) || isWithinDir(resolved, safeDataDir);
|
|
2329
|
+
if (!allowed) {
|
|
2330
|
+
throw createHttpError(400, `Path must stay within ${projectDir} or ${safeDataDir}`);
|
|
2331
|
+
}
|
|
2332
|
+
if (!fs.existsSync(resolved)) {
|
|
2333
|
+
throw createHttpError(400, `Path does not exist: ${resolved}`);
|
|
2334
|
+
}
|
|
2335
|
+
return resolved;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2243
2338
|
function createApiServer() {
|
|
2244
2339
|
const expectedApiKey = getExpectedApiKey();
|
|
2340
|
+
const expectedOperatorKey = getExpectedOperatorKey();
|
|
2245
2341
|
|
|
2246
2342
|
return http.createServer(async (req, res) => {
|
|
2247
2343
|
const parsed = new URL(req.url, 'http://localhost');
|
|
@@ -2516,6 +2612,17 @@ function createApiServer() {
|
|
|
2516
2612
|
return;
|
|
2517
2613
|
}
|
|
2518
2614
|
|
|
2615
|
+
if (isGetLikeRequest && pathname === '/.well-known/llms.txt') {
|
|
2616
|
+
const llmsTxtPath = path.join(__dirname, '..', '..', '.well-known', 'llms.txt');
|
|
2617
|
+
try {
|
|
2618
|
+
const content = fs.readFileSync(llmsTxtPath, 'utf8');
|
|
2619
|
+
sendText(res, 200, content, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=86400' }, { headOnly: isHeadRequest });
|
|
2620
|
+
} catch {
|
|
2621
|
+
sendJson(res, 404, { error: 'llms.txt not found' });
|
|
2622
|
+
}
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2519
2626
|
if (isGetLikeRequest && pathname === '/sitemap.xml') {
|
|
2520
2627
|
sendText(res, 200, renderSitemapXml(hostedConfig), {
|
|
2521
2628
|
'Content-Type': 'application/xml; charset=utf-8',
|
|
@@ -2525,6 +2632,22 @@ function createApiServer() {
|
|
|
2525
2632
|
return;
|
|
2526
2633
|
}
|
|
2527
2634
|
|
|
2635
|
+
if (isGetLikeRequest && pathname === '/llm-context.md') {
|
|
2636
|
+
const llmContextPath = path.resolve(__dirname, '../../public/llm-context.md');
|
|
2637
|
+
try {
|
|
2638
|
+
const content = fs.readFileSync(llmContextPath, 'utf8');
|
|
2639
|
+
sendText(res, 200, content, {
|
|
2640
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
2641
|
+
'X-Robots-Tag': 'all',
|
|
2642
|
+
}, {
|
|
2643
|
+
headOnly: isHeadRequest,
|
|
2644
|
+
});
|
|
2645
|
+
} catch (_err) {
|
|
2646
|
+
sendJson(res, 404, { error: 'Not found' });
|
|
2647
|
+
}
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2528
2651
|
// Quick feedback capture via GET — for statusline clickable links
|
|
2529
2652
|
if (isGetLikeRequest && pathname === '/feedback/quick') {
|
|
2530
2653
|
const signal = parsed.searchParams.get('signal');
|
|
@@ -2852,6 +2975,28 @@ async function addContext(){
|
|
|
2852
2975
|
return;
|
|
2853
2976
|
}
|
|
2854
2977
|
|
|
2978
|
+
if (isGetLikeRequest && pathname.startsWith('/guides/')) {
|
|
2979
|
+
try {
|
|
2980
|
+
const slug = pathname.replace('/guides/', '').replace(/[^a-z0-9-]/g, '');
|
|
2981
|
+
const guidePath = path.join(GUIDES_DIR, `${slug}.html`);
|
|
2982
|
+
if (!guidePath.startsWith(GUIDES_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
|
|
2983
|
+
const html = fs.readFileSync(guidePath, 'utf-8');
|
|
2984
|
+
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
2985
|
+
} catch { sendJson(res, 404, { error: 'Guide not found' }); }
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
if (isGetLikeRequest && pathname.startsWith('/compare/') && pathname !== '/compare') {
|
|
2990
|
+
try {
|
|
2991
|
+
const slug = pathname.replace('/compare/', '').replace(/[^a-z0-9-]/g, '');
|
|
2992
|
+
const comparePath = path.join(COMPARE_DIR, `${slug}.html`);
|
|
2993
|
+
if (!comparePath.startsWith(COMPARE_DIR)) { sendJson(res, 403, { error: 'Forbidden' }); return; }
|
|
2994
|
+
const html = fs.readFileSync(comparePath, 'utf-8');
|
|
2995
|
+
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
2996
|
+
} catch { sendJson(res, 404, { error: 'Comparison not found' }); }
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
|
|
2855
3000
|
if (isGetLikeRequest && pathname === '/') {
|
|
2856
3001
|
if (wantsJson(req, parsed)) {
|
|
2857
3002
|
sendJson(res, 200, {
|
|
@@ -2859,7 +3004,7 @@ async function addContext(){
|
|
|
2859
3004
|
version: pkg.version,
|
|
2860
3005
|
status: 'ok',
|
|
2861
3006
|
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'],
|
|
3007
|
+
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
3008
|
}, {}, {
|
|
2864
3009
|
headOnly: isHeadRequest,
|
|
2865
3010
|
});
|
|
@@ -4054,6 +4199,34 @@ async function addContext(){
|
|
|
4054
4199
|
return;
|
|
4055
4200
|
}
|
|
4056
4201
|
|
|
4202
|
+
if (req.method === 'GET' && pathname === '/v1/documents') {
|
|
4203
|
+
const limit = Number(parsed.searchParams.get('limit') || 20);
|
|
4204
|
+
const query = parsed.searchParams.get('q') || parsed.searchParams.get('query') || '';
|
|
4205
|
+
const tag = parsed.searchParams.get('tag') || '';
|
|
4206
|
+
const results = listImportedDocuments({
|
|
4207
|
+
feedbackDir: requestFeedbackDir,
|
|
4208
|
+
limit: Number.isFinite(limit) ? limit : 20,
|
|
4209
|
+
query,
|
|
4210
|
+
tag,
|
|
4211
|
+
});
|
|
4212
|
+
sendJson(res, 200, results);
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
{
|
|
4217
|
+
const documentId = normalizeDocumentIdFromPath(pathname);
|
|
4218
|
+
if (req.method === 'GET' && documentId) {
|
|
4219
|
+
const document = readImportedDocument(documentId, {
|
|
4220
|
+
feedbackDir: requestFeedbackDir,
|
|
4221
|
+
});
|
|
4222
|
+
if (!document) {
|
|
4223
|
+
throw createHttpError(404, `Imported document not found: ${documentId}`);
|
|
4224
|
+
}
|
|
4225
|
+
sendJson(res, 200, { document });
|
|
4226
|
+
return;
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4057
4230
|
if (req.method === 'POST' && pathname === '/v1/feedback/capture') {
|
|
4058
4231
|
const captureLimit = checkLimit('capture_feedback');
|
|
4059
4232
|
if (!captureLimit.allowed) {
|
|
@@ -4127,6 +4300,31 @@ async function addContext(){
|
|
|
4127
4300
|
return;
|
|
4128
4301
|
}
|
|
4129
4302
|
|
|
4303
|
+
if (req.method === 'POST' && pathname === '/v1/documents/import') {
|
|
4304
|
+
const body = await parseJsonBody(req, 2 * 1024 * 1024);
|
|
4305
|
+
const document = importDocument({
|
|
4306
|
+
filePath: body.filePath
|
|
4307
|
+
? resolveDocumentImportFilePath(body.filePath, {
|
|
4308
|
+
req,
|
|
4309
|
+
parsed,
|
|
4310
|
+
safeDataDir: requestSafeDataDir,
|
|
4311
|
+
})
|
|
4312
|
+
: null,
|
|
4313
|
+
content: normalizeNullableText(body.content),
|
|
4314
|
+
title: normalizeNullableText(body.title),
|
|
4315
|
+
sourceFormat: normalizeNullableText(body.sourceFormat),
|
|
4316
|
+
sourceUrl: normalizeNullableText(body.sourceUrl),
|
|
4317
|
+
tags: extractTags(body.tags),
|
|
4318
|
+
proposeGates: body.proposeGates !== false,
|
|
4319
|
+
feedbackDir: requestFeedbackDir,
|
|
4320
|
+
});
|
|
4321
|
+
sendJson(res, 201, {
|
|
4322
|
+
ok: true,
|
|
4323
|
+
document,
|
|
4324
|
+
});
|
|
4325
|
+
return;
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4130
4328
|
if (req.method === 'POST' && pathname === '/v1/dpo/export') {
|
|
4131
4329
|
const body = await parseJsonBody(req);
|
|
4132
4330
|
const paths = resolveDpoExportPaths(body, {
|
|
@@ -4362,14 +4560,14 @@ async function addContext(){
|
|
|
4362
4560
|
return;
|
|
4363
4561
|
}
|
|
4364
4562
|
|
|
4365
|
-
// GET /v1/billing/summary — admin
|
|
4563
|
+
// GET /v1/billing/summary — operator billing summary (admin key or operator key)
|
|
4366
4564
|
if (req.method === 'GET' && pathname === '/v1/billing/summary') {
|
|
4367
|
-
if (!
|
|
4565
|
+
if (!isBillingSummaryAuthorized(req, expectedApiKey, expectedOperatorKey)) {
|
|
4368
4566
|
sendProblem(res, {
|
|
4369
4567
|
type: PROBLEM_TYPES.FORBIDDEN,
|
|
4370
4568
|
title: 'Forbidden',
|
|
4371
4569
|
status: 403,
|
|
4372
|
-
detail: 'Admin API key required for this endpoint.',
|
|
4570
|
+
detail: 'Admin or operator API key required for this endpoint.',
|
|
4373
4571
|
});
|
|
4374
4572
|
return;
|
|
4375
4573
|
}
|
|
@@ -4569,6 +4767,85 @@ async function addContext(){
|
|
|
4569
4767
|
return;
|
|
4570
4768
|
}
|
|
4571
4769
|
|
|
4770
|
+
if (req.method === 'POST' && pathname === '/v1/decisions/evaluate') {
|
|
4771
|
+
const body = await parseJsonBody(req);
|
|
4772
|
+
if (!body.toolName) {
|
|
4773
|
+
sendProblem(res, {
|
|
4774
|
+
type: PROBLEM_TYPES.BAD_REQUEST,
|
|
4775
|
+
title: 'Bad Request',
|
|
4776
|
+
status: 400,
|
|
4777
|
+
detail: 'toolName is required.',
|
|
4778
|
+
});
|
|
4779
|
+
return;
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4782
|
+
const report = evaluateWorkflowSentinel(body.toolName, {
|
|
4783
|
+
command: body.command,
|
|
4784
|
+
path: body.filePath,
|
|
4785
|
+
changed_files: Array.isArray(body.changedFiles) ? body.changedFiles : [],
|
|
4786
|
+
repoPath: body.repoPath,
|
|
4787
|
+
baseBranch: body.baseBranch,
|
|
4788
|
+
}, {
|
|
4789
|
+
repoPath: body.repoPath,
|
|
4790
|
+
baseBranch: body.baseBranch,
|
|
4791
|
+
affectedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : undefined,
|
|
4792
|
+
requirePrForReleaseSensitive: body.requirePrForReleaseSensitive === true,
|
|
4793
|
+
requireVersionNotBehindBase: body.requireVersionNotBehindBase === true,
|
|
4794
|
+
governanceState: getScopeState(),
|
|
4795
|
+
feedbackDir: requestFeedbackDir,
|
|
4796
|
+
});
|
|
4797
|
+
const evaluation = recordDecisionEvaluation(report, {
|
|
4798
|
+
source: 'api',
|
|
4799
|
+
toolName: body.toolName,
|
|
4800
|
+
toolInput: {
|
|
4801
|
+
command: body.command,
|
|
4802
|
+
filePath: body.filePath,
|
|
4803
|
+
changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
|
|
4804
|
+
repoPath: body.repoPath,
|
|
4805
|
+
baseBranch: body.baseBranch,
|
|
4806
|
+
},
|
|
4807
|
+
changedFiles: Array.isArray(body.changedFiles) ? body.changedFiles : [],
|
|
4808
|
+
}, {
|
|
4809
|
+
feedbackDir: requestFeedbackDir,
|
|
4810
|
+
});
|
|
4811
|
+
report.actionId = evaluation.actionId;
|
|
4812
|
+
if (report.decisionControl) report.decisionControl.actionId = evaluation.actionId;
|
|
4813
|
+
sendJson(res, 200, report);
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
4816
|
+
|
|
4817
|
+
if (req.method === 'POST' && pathname === '/v1/decisions/outcome') {
|
|
4818
|
+
const body = await parseJsonBody(req);
|
|
4819
|
+
if (!body.actionId || !body.outcome) {
|
|
4820
|
+
sendProblem(res, {
|
|
4821
|
+
type: PROBLEM_TYPES.BAD_REQUEST,
|
|
4822
|
+
title: 'Bad Request',
|
|
4823
|
+
status: 400,
|
|
4824
|
+
detail: 'actionId and outcome are required.',
|
|
4825
|
+
});
|
|
4826
|
+
return;
|
|
4827
|
+
}
|
|
4828
|
+
const outcome = recordDecisionOutcome({
|
|
4829
|
+
actionId: body.actionId,
|
|
4830
|
+
outcome: body.outcome,
|
|
4831
|
+
actualDecision: body.actualDecision,
|
|
4832
|
+
actor: body.actor,
|
|
4833
|
+
notes: body.notes,
|
|
4834
|
+
metadata: body.metadata,
|
|
4835
|
+
latencyMs: body.latencyMs,
|
|
4836
|
+
source: 'api',
|
|
4837
|
+
}, {
|
|
4838
|
+
feedbackDir: requestFeedbackDir,
|
|
4839
|
+
});
|
|
4840
|
+
sendJson(res, 200, outcome);
|
|
4841
|
+
return;
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4844
|
+
if (req.method === 'GET' && pathname === '/v1/decisions/metrics') {
|
|
4845
|
+
sendJson(res, 200, computeDecisionMetrics(requestFeedbackDir));
|
|
4846
|
+
return;
|
|
4847
|
+
}
|
|
4848
|
+
|
|
4572
4849
|
// GET /v1/settings/status -- Resolved settings hierarchy with origin metadata
|
|
4573
4850
|
if (req.method === 'GET' && pathname === '/v1/settings/status') {
|
|
4574
4851
|
sendJson(res, 200, getSettingsStatus());
|
|
@@ -4599,9 +4876,9 @@ async function addContext(){
|
|
|
4599
4876
|
return;
|
|
4600
4877
|
}
|
|
4601
4878
|
|
|
4602
|
-
// POST /webhook/stripe —
|
|
4603
|
-
//
|
|
4604
|
-
//
|
|
4879
|
+
// POST /webhook/stripe — legacy Stripe event log bridge kept for backward compatibility.
|
|
4880
|
+
// When STRIPE_WEBHOOK_SECRET is configured, verify the same Stripe signature used by
|
|
4881
|
+
// the /v1/billing/webhook route before touching any payload.
|
|
4605
4882
|
if (req.method === 'POST' && pathname === '/webhook/stripe') {
|
|
4606
4883
|
try {
|
|
4607
4884
|
const rawBody = await new Promise((resolve, reject) => {
|
|
@@ -4610,6 +4887,18 @@ async function addContext(){
|
|
|
4610
4887
|
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
4611
4888
|
req.on('error', reject);
|
|
4612
4889
|
});
|
|
4890
|
+
|
|
4891
|
+
const sig = req.headers['stripe-signature'] || '';
|
|
4892
|
+
if (!verifyWebhookSignature(rawBody, sig)) {
|
|
4893
|
+
sendProblem(res, {
|
|
4894
|
+
type: PROBLEM_TYPES.WEBHOOK_INVALID,
|
|
4895
|
+
title: 'Invalid webhook signature',
|
|
4896
|
+
status: 400,
|
|
4897
|
+
detail: 'The webhook signature could not be verified.',
|
|
4898
|
+
});
|
|
4899
|
+
return;
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4613
4902
|
let event;
|
|
4614
4903
|
try {
|
|
4615
4904
|
event = JSON.parse(rawBody.toString('utf-8'));
|
|
Binary file
|
|
Binary file
|
|
File without changes
|