thumbgate 0.9.12 → 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 +2 -2
- 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 +118 -27
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;
|
|
@@ -3811,11 +3880,21 @@ async function addContext(){
|
|
|
3811
3880
|
return;
|
|
3812
3881
|
}
|
|
3813
3882
|
const body = await parseJsonBody(req);
|
|
3883
|
+
// Auto-include conversation window when caller doesn't provide one
|
|
3884
|
+
let chatHistory = Array.isArray(body.chatHistory) ? body.chatHistory : body.messages;
|
|
3885
|
+
if (!chatHistory || chatHistory.length === 0) {
|
|
3886
|
+
try {
|
|
3887
|
+
chatHistory = readRecentConversationWindow({
|
|
3888
|
+
feedbackDir: getSafeDataDir(),
|
|
3889
|
+
limit: 10,
|
|
3890
|
+
});
|
|
3891
|
+
} catch (_) { /* best-effort — conversation window is optional */ }
|
|
3892
|
+
}
|
|
3814
3893
|
const result = captureFeedback({
|
|
3815
3894
|
signal: body.signal,
|
|
3816
3895
|
context: body.context || '',
|
|
3817
3896
|
relatedFeedbackId: body.relatedFeedbackId,
|
|
3818
|
-
chatHistory
|
|
3897
|
+
chatHistory,
|
|
3819
3898
|
whatWentWrong: body.whatWentWrong,
|
|
3820
3899
|
whatToChange: body.whatToChange,
|
|
3821
3900
|
whatWorked: body.whatWorked,
|
|
@@ -3836,7 +3915,9 @@ async function addContext(){
|
|
|
3836
3915
|
if (req.method === 'POST' && pathname === '/v1/feedback/rules') {
|
|
3837
3916
|
const body = await parseJsonBody(req);
|
|
3838
3917
|
const minOccurrences = Number(body.minOccurrences || 2);
|
|
3839
|
-
const outputPath = body.outputPath
|
|
3918
|
+
const outputPath = body.outputPath
|
|
3919
|
+
? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
|
|
3920
|
+
: undefined;
|
|
3840
3921
|
const result = writePreventionRules(outputPath, Number.isFinite(minOccurrences) ? minOccurrences : 2);
|
|
3841
3922
|
sendJson(res, 200, {
|
|
3842
3923
|
path: result.path,
|
|
@@ -3865,20 +3946,28 @@ async function addContext(){
|
|
|
3865
3946
|
let memories = [];
|
|
3866
3947
|
|
|
3867
3948
|
if (body.inputPath) {
|
|
3868
|
-
const safeInputPath = resolveSafePath(body.inputPath, {
|
|
3949
|
+
const safeInputPath = resolveSafePath(body.inputPath, {
|
|
3950
|
+
mustExist: true,
|
|
3951
|
+
safeDataDir: requestSafeDataDir,
|
|
3952
|
+
});
|
|
3869
3953
|
const raw = fs.readFileSync(safeInputPath, 'utf-8');
|
|
3870
3954
|
const parsedMemories = JSON.parse(raw);
|
|
3871
3955
|
memories = Array.isArray(parsedMemories) ? parsedMemories : parsedMemories.memories || [];
|
|
3872
3956
|
} else {
|
|
3873
3957
|
const localPath = body.memoryLogPath
|
|
3874
|
-
? resolveSafePath(body.memoryLogPath, {
|
|
3875
|
-
|
|
3958
|
+
? resolveSafePath(body.memoryLogPath, {
|
|
3959
|
+
mustExist: true,
|
|
3960
|
+
safeDataDir: requestSafeDataDir,
|
|
3961
|
+
})
|
|
3962
|
+
: requestFeedbackPaths.MEMORY_LOG_PATH;
|
|
3876
3963
|
memories = readJSONL(localPath);
|
|
3877
3964
|
}
|
|
3878
3965
|
|
|
3879
3966
|
const result = exportDpoFromMemories(memories);
|
|
3880
3967
|
if (body.outputPath) {
|
|
3881
|
-
const safeOutputPath = resolveSafePath(body.outputPath
|
|
3968
|
+
const safeOutputPath = resolveSafePath(body.outputPath, {
|
|
3969
|
+
safeDataDir: requestSafeDataDir,
|
|
3970
|
+
});
|
|
3882
3971
|
fs.mkdirSync(path.dirname(safeOutputPath), { recursive: true });
|
|
3883
3972
|
fs.writeFileSync(safeOutputPath, result.jsonl);
|
|
3884
3973
|
}
|
|
@@ -3889,14 +3978,18 @@ async function addContext(){
|
|
|
3889
3978
|
learnings: result.learnings.length,
|
|
3890
3979
|
unpairedErrors: result.unpairedErrors.length,
|
|
3891
3980
|
unpairedLearnings: result.unpairedLearnings.length,
|
|
3892
|
-
outputPath: body.outputPath
|
|
3981
|
+
outputPath: body.outputPath
|
|
3982
|
+
? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
|
|
3983
|
+
: null,
|
|
3893
3984
|
});
|
|
3894
3985
|
return;
|
|
3895
3986
|
}
|
|
3896
3987
|
|
|
3897
3988
|
if (req.method === 'POST' && pathname === '/v1/analytics/databricks/export') {
|
|
3898
3989
|
const body = await parseJsonBody(req);
|
|
3899
|
-
const outputPath = body.outputPath
|
|
3990
|
+
const outputPath = body.outputPath
|
|
3991
|
+
? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
|
|
3992
|
+
: undefined;
|
|
3900
3993
|
const result = exportDatabricksBundle(undefined, outputPath);
|
|
3901
3994
|
sendJson(res, 200, result);
|
|
3902
3995
|
return;
|
|
@@ -3961,7 +4054,7 @@ async function addContext(){
|
|
|
3961
4054
|
// ----------------------------------------------------------------
|
|
3962
4055
|
|
|
3963
4056
|
if (req.method === 'GET' && pathname === '/v1/quality/scores') {
|
|
3964
|
-
const modelPath = path.join(
|
|
4057
|
+
const modelPath = path.join(requestSafeDataDir, 'feedback_model.json');
|
|
3965
4058
|
const model = loadModel(modelPath);
|
|
3966
4059
|
const reliability = getReliability(model);
|
|
3967
4060
|
const category = parsed.searchParams.get('category');
|
|
@@ -3981,7 +4074,7 @@ async function addContext(){
|
|
|
3981
4074
|
}
|
|
3982
4075
|
|
|
3983
4076
|
if (req.method === 'GET' && pathname === '/v1/quality/rules') {
|
|
3984
|
-
const rulesPath = path.join(
|
|
4077
|
+
const rulesPath = path.join(requestSafeDataDir, 'prevention-rules.md');
|
|
3985
4078
|
let markdown = '';
|
|
3986
4079
|
if (fs.existsSync(rulesPath)) {
|
|
3987
4080
|
markdown = fs.readFileSync(rulesPath, 'utf8').trim();
|
|
@@ -3998,7 +4091,7 @@ async function addContext(){
|
|
|
3998
4091
|
}
|
|
3999
4092
|
|
|
4000
4093
|
if (req.method === 'GET' && pathname === '/v1/quality/posteriors') {
|
|
4001
|
-
const modelPath = path.join(
|
|
4094
|
+
const modelPath = path.join(requestSafeDataDir, 'feedback_model.json');
|
|
4002
4095
|
const model = loadModel(modelPath);
|
|
4003
4096
|
const posteriors = samplePosteriors(model);
|
|
4004
4097
|
sendJson(res, 200, { posteriors });
|
|
@@ -4237,9 +4330,8 @@ async function addContext(){
|
|
|
4237
4330
|
return;
|
|
4238
4331
|
}
|
|
4239
4332
|
|
|
4240
|
-
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
4241
4333
|
const billingSummary = await getBillingSummaryLive(summaryOptions);
|
|
4242
|
-
const data = generateDashboard(
|
|
4334
|
+
const data = generateDashboard(requestFeedbackDir, {
|
|
4243
4335
|
analyticsWindow: summaryOptions,
|
|
4244
4336
|
billingSummary,
|
|
4245
4337
|
billingSource: 'live',
|
|
@@ -4265,9 +4357,8 @@ async function addContext(){
|
|
|
4265
4357
|
}
|
|
4266
4358
|
|
|
4267
4359
|
try {
|
|
4268
|
-
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
4269
4360
|
const billingSummary = await getBillingSummaryLive(summaryOptions);
|
|
4270
|
-
const data = generateDashboard(
|
|
4361
|
+
const data = generateDashboard(requestFeedbackDir, {
|
|
4271
4362
|
analyticsWindow: summaryOptions,
|
|
4272
4363
|
billingSummary,
|
|
4273
4364
|
billingSource: 'live',
|