thumbgate 0.9.13 → 0.9.14
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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +5 -3
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +35 -0
- package/package.json +1 -1
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +1 -1
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/index.html +27 -3
- package/public/learn.html +61 -0
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/feedback-history-distiller.js +7 -1
- package/scripts/feedback-loop.js +10 -4
- package/scripts/feedback-paths.js +142 -10
- package/scripts/feedback-root-consolidator.js +18 -4
- package/scripts/post-everywhere.js +10 -0
- package/scripts/seo-gsd.js +217 -4
- package/scripts/statusline-cache-path.js +9 -6
- package/src/api/server.js +107 -26
package/src/api/server.js
CHANGED
|
@@ -13,6 +13,10 @@ const {
|
|
|
13
13
|
getFeedbackPaths,
|
|
14
14
|
appendDiagnosticRecord,
|
|
15
15
|
} = require('../../scripts/feedback-loop');
|
|
16
|
+
const {
|
|
17
|
+
readActiveProjectState,
|
|
18
|
+
resolveProjectDir,
|
|
19
|
+
} = require('../../scripts/feedback-paths');
|
|
16
20
|
const {
|
|
17
21
|
readRecentConversationWindow,
|
|
18
22
|
} = require('../../scripts/feedback-history-distiller');
|
|
@@ -203,8 +207,55 @@ function computeConversionStats(events) {
|
|
|
203
207
|
}
|
|
204
208
|
// ---------------------------------------------------------------------------
|
|
205
209
|
|
|
206
|
-
function
|
|
207
|
-
const
|
|
210
|
+
function getRequestedProjectSelection(req, parsed) {
|
|
211
|
+
const projectFromQuery = parsed && parsed.searchParams
|
|
212
|
+
? parsed.searchParams.get('project')
|
|
213
|
+
: null;
|
|
214
|
+
const projectFromHeaders = req && req.headers
|
|
215
|
+
? req.headers['x-thumbgate-project-dir'] || req.headers['x-thumbgate-project']
|
|
216
|
+
: null;
|
|
217
|
+
return projectFromQuery || projectFromHeaders || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getEffectiveRequestedProjectSelection(req, parsed) {
|
|
221
|
+
return isProjectSelectionAllowed(req, parsed)
|
|
222
|
+
? getRequestedProjectSelection(req, parsed)
|
|
223
|
+
: null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isProjectSelectionAllowed(req, parsed) {
|
|
227
|
+
const explicitProject = getRequestedProjectSelection(req, parsed);
|
|
228
|
+
if (!explicitProject) return true;
|
|
229
|
+
return isLoopbackHost(getRequestHostHeader(req));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveRequestProjectDir(req, parsed) {
|
|
233
|
+
const explicitProject = getEffectiveRequestedProjectSelection(req, parsed);
|
|
234
|
+
return resolveProjectDir({
|
|
235
|
+
projectDir: explicitProject,
|
|
236
|
+
env: process.env,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function shouldPreferProjectScopedFeedback(req, parsed) {
|
|
241
|
+
const explicitProject = getEffectiveRequestedProjectSelection(req, parsed);
|
|
242
|
+
if (explicitProject) return true;
|
|
243
|
+
if (process.env.THUMBGATE_PROJECT_DIR || process.env.CLAUDE_PROJECT_DIR) return true;
|
|
244
|
+
if (process.env.THUMBGATE_FEEDBACK_DIR) return false;
|
|
245
|
+
return Boolean(readActiveProjectState({ env: process.env }));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getRequestFeedbackPaths(req, parsed) {
|
|
249
|
+
const explicitProject = getEffectiveRequestedProjectSelection(req, parsed);
|
|
250
|
+
return getFeedbackPaths({
|
|
251
|
+
projectDir: resolveRequestProjectDir(req, parsed),
|
|
252
|
+
explicitProjectDir: explicitProject,
|
|
253
|
+
skipExplicitFeedbackDir: shouldPreferProjectScopedFeedback(req, parsed),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getSafeDataDir(req, parsed) {
|
|
258
|
+
const { FEEDBACK_LOG_PATH } = getRequestFeedbackPaths(req, parsed);
|
|
208
259
|
return path.resolve(path.dirname(FEEDBACK_LOG_PATH));
|
|
209
260
|
}
|
|
210
261
|
|
|
@@ -857,6 +908,14 @@ function getPublicOrigin(req) {
|
|
|
857
908
|
return `${proto}://${host}`;
|
|
858
909
|
}
|
|
859
910
|
|
|
911
|
+
function getRequestHostHeader(req) {
|
|
912
|
+
const forwardedHost = req.headers['x-forwarded-host'];
|
|
913
|
+
if (Array.isArray(forwardedHost)) {
|
|
914
|
+
return forwardedHost[0] || req.headers.host || '';
|
|
915
|
+
}
|
|
916
|
+
return forwardedHost || req.headers.host || '';
|
|
917
|
+
}
|
|
918
|
+
|
|
860
919
|
function isLoopbackHost(hostValue) {
|
|
861
920
|
const rawHost = String(hostValue || '').split(',')[0].trim();
|
|
862
921
|
if (!rawHost) {
|
|
@@ -2094,10 +2153,10 @@ function extractTags(input) {
|
|
|
2094
2153
|
return [];
|
|
2095
2154
|
}
|
|
2096
2155
|
|
|
2097
|
-
function resolveSafePath(inputPath, { mustExist = false } = {}) {
|
|
2156
|
+
function resolveSafePath(inputPath, { mustExist = false, safeDataDir } = {}) {
|
|
2098
2157
|
const allowExternal = process.env.THUMBGATE_ALLOW_EXTERNAL_PATHS === 'true';
|
|
2099
2158
|
const resolved = path.resolve(String(inputPath || ''));
|
|
2100
|
-
const SAFE_DATA_DIR = getSafeDataDir();
|
|
2159
|
+
const SAFE_DATA_DIR = safeDataDir || getSafeDataDir();
|
|
2101
2160
|
const inSafeRoot = resolved === SAFE_DATA_DIR || resolved.startsWith(`${SAFE_DATA_DIR}${path.sep}`);
|
|
2102
2161
|
|
|
2103
2162
|
if (!allowExternal && !inSafeRoot) {
|
|
@@ -2121,6 +2180,13 @@ function createApiServer() {
|
|
|
2121
2180
|
const isGetLikeRequest = req.method === 'GET' || isHeadRequest;
|
|
2122
2181
|
const publicOrigin = getPublicOrigin(req);
|
|
2123
2182
|
const hostedConfig = resolveHostedBillingConfig({ requestOrigin: publicOrigin });
|
|
2183
|
+
if (!isProjectSelectionAllowed(req, parsed)) {
|
|
2184
|
+
sendJson(res, 403, { error: 'project selection is only available on localhost requests' });
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
const requestFeedbackPaths = getRequestFeedbackPaths(req, parsed);
|
|
2188
|
+
const requestFeedbackDir = requestFeedbackPaths.FEEDBACK_DIR;
|
|
2189
|
+
const requestSafeDataDir = getSafeDataDir(req, parsed);
|
|
2124
2190
|
|
|
2125
2191
|
// Public MCP endpoint — responds to Smithery registry scanning and MCP initialize
|
|
2126
2192
|
// The initialize handshake is unauthenticated; subsequent tool calls require Bearer auth
|
|
@@ -2394,7 +2460,7 @@ function createApiServer() {
|
|
|
2394
2460
|
const signal = parsed.searchParams.get('signal');
|
|
2395
2461
|
if (signal === 'up' || signal === 'down') {
|
|
2396
2462
|
const chatHistory = readRecentConversationWindow({
|
|
2397
|
-
feedbackDir:
|
|
2463
|
+
feedbackDir: requestSafeDataDir,
|
|
2398
2464
|
limit: 10,
|
|
2399
2465
|
});
|
|
2400
2466
|
const result = captureFeedback({
|
|
@@ -2484,7 +2550,7 @@ async function addContext(){
|
|
|
2484
2550
|
sendJson(res, 400, { error: 'relatedFeedbackId is required' });
|
|
2485
2551
|
return;
|
|
2486
2552
|
}
|
|
2487
|
-
const feedbackDir =
|
|
2553
|
+
const feedbackDir = requestSafeDataDir;
|
|
2488
2554
|
const detailField = signal === 'down' ? 'whatWentWrong' : 'whatWorked';
|
|
2489
2555
|
const updated = updateLessonRecord(feedbackDir, relatedFeedbackId, (existing) => {
|
|
2490
2556
|
const nextTags = Array.from(new Set([
|
|
@@ -2536,7 +2602,7 @@ async function addContext(){
|
|
|
2536
2602
|
const lessonUpdateMatch = pathname.match(/^\/lessons\/([^/]+)\/update$/);
|
|
2537
2603
|
if (req.method === 'POST' && lessonUpdateMatch) {
|
|
2538
2604
|
const lessonId = decodeURIComponent(lessonUpdateMatch[1]);
|
|
2539
|
-
const feedbackDir =
|
|
2605
|
+
const feedbackDir = requestSafeDataDir;
|
|
2540
2606
|
const body = await parseJsonBody(req);
|
|
2541
2607
|
const record = findRecordById(lessonId, feedbackDir);
|
|
2542
2608
|
if (!record) {
|
|
@@ -2568,7 +2634,7 @@ async function addContext(){
|
|
|
2568
2634
|
const lessonDeleteMatch = pathname.match(/^\/lessons\/([^/]+)\/delete$/);
|
|
2569
2635
|
if (req.method === 'POST' && lessonDeleteMatch) {
|
|
2570
2636
|
const lessonId = decodeURIComponent(lessonDeleteMatch[1]);
|
|
2571
|
-
const feedbackDir =
|
|
2637
|
+
const feedbackDir = requestSafeDataDir;
|
|
2572
2638
|
const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
|
|
2573
2639
|
const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
|
|
2574
2640
|
const deletedMemory = deleteRecordFromJsonl(memoryLogPath, lessonId);
|
|
@@ -2585,7 +2651,7 @@ async function addContext(){
|
|
|
2585
2651
|
const lessonDetailMatch = pathname.match(/^\/lessons\/([^/]+)$/);
|
|
2586
2652
|
if (isGetLikeRequest && lessonDetailMatch && lessonDetailMatch[1] !== '') {
|
|
2587
2653
|
const lessonId = decodeURIComponent(lessonDetailMatch[1]);
|
|
2588
|
-
const feedbackDir =
|
|
2654
|
+
const feedbackDir = requestSafeDataDir;
|
|
2589
2655
|
const record = findRecordById(lessonId, feedbackDir);
|
|
2590
2656
|
if (!record) {
|
|
2591
2657
|
sendHtml(res, 404, renderLessonDetailHtml(null, lessonId));
|
|
@@ -2959,7 +3025,7 @@ async function addContext(){
|
|
|
2959
3025
|
}
|
|
2960
3026
|
|
|
2961
3027
|
if (isGetLikeRequest && pathname === '/healthz') {
|
|
2962
|
-
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH } =
|
|
3028
|
+
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH } = requestFeedbackPaths;
|
|
2963
3029
|
sendJson(res, 200, {
|
|
2964
3030
|
status: 'ok',
|
|
2965
3031
|
feedbackLogPath: FEEDBACK_LOG_PATH,
|
|
@@ -3497,7 +3563,7 @@ async function addContext(){
|
|
|
3497
3563
|
|
|
3498
3564
|
try {
|
|
3499
3565
|
if (req.method === 'GET' && pathname === '/v1/feedback/stats') {
|
|
3500
|
-
sendJson(res, 200, analyzeFeedback());
|
|
3566
|
+
sendJson(res, 200, analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH));
|
|
3501
3567
|
return;
|
|
3502
3568
|
}
|
|
3503
3569
|
|
|
@@ -3740,7 +3806,9 @@ async function addContext(){
|
|
|
3740
3806
|
|
|
3741
3807
|
if (req.method === 'GET' && pathname === '/v1/feedback/summary') {
|
|
3742
3808
|
const recent = Number(parsed.searchParams.get('recent') || 20);
|
|
3743
|
-
const summary = feedbackSummary(Number.isFinite(recent) ? recent : 20
|
|
3809
|
+
const summary = feedbackSummary(Number.isFinite(recent) ? recent : 20, {
|
|
3810
|
+
feedbackDir: requestFeedbackDir,
|
|
3811
|
+
});
|
|
3744
3812
|
sendJson(res, 200, { summary });
|
|
3745
3813
|
return;
|
|
3746
3814
|
}
|
|
@@ -3757,6 +3825,7 @@ async function addContext(){
|
|
|
3757
3825
|
limit: Number.isFinite(limit) ? limit : 10,
|
|
3758
3826
|
category,
|
|
3759
3827
|
tags,
|
|
3828
|
+
feedbackDir: requestFeedbackDir,
|
|
3760
3829
|
});
|
|
3761
3830
|
sendJson(res, 200, results);
|
|
3762
3831
|
return;
|
|
@@ -3846,7 +3915,9 @@ async function addContext(){
|
|
|
3846
3915
|
if (req.method === 'POST' && pathname === '/v1/feedback/rules') {
|
|
3847
3916
|
const body = await parseJsonBody(req);
|
|
3848
3917
|
const minOccurrences = Number(body.minOccurrences || 2);
|
|
3849
|
-
const outputPath = body.outputPath
|
|
3918
|
+
const outputPath = body.outputPath
|
|
3919
|
+
? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
|
|
3920
|
+
: undefined;
|
|
3850
3921
|
const result = writePreventionRules(outputPath, Number.isFinite(minOccurrences) ? minOccurrences : 2);
|
|
3851
3922
|
sendJson(res, 200, {
|
|
3852
3923
|
path: result.path,
|
|
@@ -3875,20 +3946,28 @@ async function addContext(){
|
|
|
3875
3946
|
let memories = [];
|
|
3876
3947
|
|
|
3877
3948
|
if (body.inputPath) {
|
|
3878
|
-
const safeInputPath = resolveSafePath(body.inputPath, {
|
|
3949
|
+
const safeInputPath = resolveSafePath(body.inputPath, {
|
|
3950
|
+
mustExist: true,
|
|
3951
|
+
safeDataDir: requestSafeDataDir,
|
|
3952
|
+
});
|
|
3879
3953
|
const raw = fs.readFileSync(safeInputPath, 'utf-8');
|
|
3880
3954
|
const parsedMemories = JSON.parse(raw);
|
|
3881
3955
|
memories = Array.isArray(parsedMemories) ? parsedMemories : parsedMemories.memories || [];
|
|
3882
3956
|
} else {
|
|
3883
3957
|
const localPath = body.memoryLogPath
|
|
3884
|
-
? resolveSafePath(body.memoryLogPath, {
|
|
3885
|
-
|
|
3958
|
+
? resolveSafePath(body.memoryLogPath, {
|
|
3959
|
+
mustExist: true,
|
|
3960
|
+
safeDataDir: requestSafeDataDir,
|
|
3961
|
+
})
|
|
3962
|
+
: requestFeedbackPaths.MEMORY_LOG_PATH;
|
|
3886
3963
|
memories = readJSONL(localPath);
|
|
3887
3964
|
}
|
|
3888
3965
|
|
|
3889
3966
|
const result = exportDpoFromMemories(memories);
|
|
3890
3967
|
if (body.outputPath) {
|
|
3891
|
-
const safeOutputPath = resolveSafePath(body.outputPath
|
|
3968
|
+
const safeOutputPath = resolveSafePath(body.outputPath, {
|
|
3969
|
+
safeDataDir: requestSafeDataDir,
|
|
3970
|
+
});
|
|
3892
3971
|
fs.mkdirSync(path.dirname(safeOutputPath), { recursive: true });
|
|
3893
3972
|
fs.writeFileSync(safeOutputPath, result.jsonl);
|
|
3894
3973
|
}
|
|
@@ -3899,14 +3978,18 @@ async function addContext(){
|
|
|
3899
3978
|
learnings: result.learnings.length,
|
|
3900
3979
|
unpairedErrors: result.unpairedErrors.length,
|
|
3901
3980
|
unpairedLearnings: result.unpairedLearnings.length,
|
|
3902
|
-
outputPath: body.outputPath
|
|
3981
|
+
outputPath: body.outputPath
|
|
3982
|
+
? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
|
|
3983
|
+
: null,
|
|
3903
3984
|
});
|
|
3904
3985
|
return;
|
|
3905
3986
|
}
|
|
3906
3987
|
|
|
3907
3988
|
if (req.method === 'POST' && pathname === '/v1/analytics/databricks/export') {
|
|
3908
3989
|
const body = await parseJsonBody(req);
|
|
3909
|
-
const outputPath = body.outputPath
|
|
3990
|
+
const outputPath = body.outputPath
|
|
3991
|
+
? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
|
|
3992
|
+
: undefined;
|
|
3910
3993
|
const result = exportDatabricksBundle(undefined, outputPath);
|
|
3911
3994
|
sendJson(res, 200, result);
|
|
3912
3995
|
return;
|
|
@@ -3971,7 +4054,7 @@ async function addContext(){
|
|
|
3971
4054
|
// ----------------------------------------------------------------
|
|
3972
4055
|
|
|
3973
4056
|
if (req.method === 'GET' && pathname === '/v1/quality/scores') {
|
|
3974
|
-
const modelPath = path.join(
|
|
4057
|
+
const modelPath = path.join(requestSafeDataDir, 'feedback_model.json');
|
|
3975
4058
|
const model = loadModel(modelPath);
|
|
3976
4059
|
const reliability = getReliability(model);
|
|
3977
4060
|
const category = parsed.searchParams.get('category');
|
|
@@ -3991,7 +4074,7 @@ async function addContext(){
|
|
|
3991
4074
|
}
|
|
3992
4075
|
|
|
3993
4076
|
if (req.method === 'GET' && pathname === '/v1/quality/rules') {
|
|
3994
|
-
const rulesPath = path.join(
|
|
4077
|
+
const rulesPath = path.join(requestSafeDataDir, 'prevention-rules.md');
|
|
3995
4078
|
let markdown = '';
|
|
3996
4079
|
if (fs.existsSync(rulesPath)) {
|
|
3997
4080
|
markdown = fs.readFileSync(rulesPath, 'utf8').trim();
|
|
@@ -4008,7 +4091,7 @@ async function addContext(){
|
|
|
4008
4091
|
}
|
|
4009
4092
|
|
|
4010
4093
|
if (req.method === 'GET' && pathname === '/v1/quality/posteriors') {
|
|
4011
|
-
const modelPath = path.join(
|
|
4094
|
+
const modelPath = path.join(requestSafeDataDir, 'feedback_model.json');
|
|
4012
4095
|
const model = loadModel(modelPath);
|
|
4013
4096
|
const posteriors = samplePosteriors(model);
|
|
4014
4097
|
sendJson(res, 200, { posteriors });
|
|
@@ -4247,9 +4330,8 @@ async function addContext(){
|
|
|
4247
4330
|
return;
|
|
4248
4331
|
}
|
|
4249
4332
|
|
|
4250
|
-
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
4251
4333
|
const billingSummary = await getBillingSummaryLive(summaryOptions);
|
|
4252
|
-
const data = generateDashboard(
|
|
4334
|
+
const data = generateDashboard(requestFeedbackDir, {
|
|
4253
4335
|
analyticsWindow: summaryOptions,
|
|
4254
4336
|
billingSummary,
|
|
4255
4337
|
billingSource: 'live',
|
|
@@ -4275,9 +4357,8 @@ async function addContext(){
|
|
|
4275
4357
|
}
|
|
4276
4358
|
|
|
4277
4359
|
try {
|
|
4278
|
-
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
4279
4360
|
const billingSummary = await getBillingSummaryLive(summaryOptions);
|
|
4280
|
-
const data = generateDashboard(
|
|
4361
|
+
const data = generateDashboard(requestFeedbackDir, {
|
|
4281
4362
|
analyticsWindow: summaryOptions,
|
|
4282
4363
|
billingSummary,
|
|
4283
4364
|
billingSource: 'live',
|