thumbgate 0.9.13 → 1.0.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +6 -3
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +105 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/forge/forge.yaml +28 -0
- package/adapters/mcp/server-stdio.js +32 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +53 -3
- package/config/mcp-allowlists.json +10 -0
- package/openapi/openapi.yaml +105 -0
- package/package.json +4 -4
- package/plugins/amp-skill/INSTALL.md +3 -4
- package/plugins/amp-skill/SKILL.md +0 -1
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-skill/INSTALL.md +1 -2
- 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/blog.html +1 -0
- package/public/dashboard.html +1 -1
- package/public/guide.html +1 -1
- package/public/index.html +29 -5
- package/public/learn/agent-harness-pattern.html +1 -1
- package/public/learn/ai-agent-persistent-memory.html +1 -1
- package/public/learn/mcp-pre-action-gates-explained.html +1 -1
- package/public/learn/stop-ai-agent-force-push.html +1 -1
- package/public/learn/vibe-coding-safety-net.html +1 -1
- package/public/learn.html +62 -1
- package/public/lessons.html +1 -1
- package/public/pro.html +1 -1
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/agent-security-hardening.js +4 -4
- package/scripts/async-job-runner.js +84 -24
- package/scripts/auto-wire-hooks.js +59 -1
- package/scripts/context-manager.js +330 -0
- package/scripts/dashboard.js +1 -1
- package/scripts/distribution-surfaces.js +12 -0
- package/scripts/ensure-repo-bootstrap.js +15 -14
- 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/gates-engine.js +96 -10
- package/scripts/hook-auto-capture.sh +1 -1
- package/scripts/hosted-job-launcher.js +260 -0
- package/scripts/managed-dpo-export.js +91 -0
- package/scripts/obsidian-export.js +0 -1
- package/scripts/operational-integrity.js +50 -7
- package/scripts/post-everywhere.js +10 -0
- package/scripts/prove-lancedb.js +62 -4
- package/scripts/publish-decision.js +16 -0
- package/scripts/self-healing-check.js +6 -1
- package/scripts/seo-gsd.js +217 -4
- package/scripts/social-analytics/load-env.js +33 -2
- package/scripts/social-analytics/store.js +200 -2
- package/scripts/statusline-cache-path.js +9 -6
- package/scripts/sync-version.js +18 -11
- package/scripts/tool-registry.js +37 -0
- package/scripts/train_from_feedback.py +0 -4
- package/scripts/workflow-sentinel.js +793 -0
- package/src/api/server.js +297 -38
- /package/scripts/{rlhf_session_start.sh → thumbgate_session_start.sh} +0 -0
package/scripts/seo-gsd.js
CHANGED
|
@@ -67,12 +67,30 @@ const HIGH_ROI_QUERY_SEEDS = [
|
|
|
67
67
|
source: 'seed',
|
|
68
68
|
notes: 'Problem-led copy that maps to landing-page positioning.',
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
query: 'cursor prevent repeated mistakes',
|
|
72
|
+
businessValue: 87,
|
|
73
|
+
source: 'seed',
|
|
74
|
+
notes: 'High-intent Cursor workflow page for developers already feeling repeat-failure pain.',
|
|
75
|
+
},
|
|
70
76
|
{
|
|
71
77
|
query: 'claude code prevent repeated mistakes',
|
|
72
78
|
businessValue: 86,
|
|
73
79
|
source: 'seed',
|
|
74
80
|
notes: 'High-intent pain query for Claude Code buyers.',
|
|
75
81
|
},
|
|
82
|
+
{
|
|
83
|
+
query: 'codex cli guardrails',
|
|
84
|
+
businessValue: 84,
|
|
85
|
+
source: 'seed',
|
|
86
|
+
notes: 'Guardrail-focused page for Codex CLI buyers who want prevention, not just memory.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
query: 'gemini cli feedback memory',
|
|
90
|
+
businessValue: 82,
|
|
91
|
+
source: 'seed',
|
|
92
|
+
notes: 'Integration page for Gemini CLI users who need memory plus enforcement.',
|
|
93
|
+
},
|
|
76
94
|
];
|
|
77
95
|
|
|
78
96
|
const PAGE_BLUEPRINTS = [
|
|
@@ -225,6 +243,55 @@ const PAGE_BLUEPRINTS = [
|
|
|
225
243
|
],
|
|
226
244
|
relatedPaths: ['/compare/speclock', '/guides/claude-code-feedback'],
|
|
227
245
|
},
|
|
246
|
+
{
|
|
247
|
+
query: 'stop ai coding agents from repeating mistakes',
|
|
248
|
+
path: '/guides/stop-repeated-ai-agent-mistakes',
|
|
249
|
+
pageType: 'guide',
|
|
250
|
+
pillar: 'pre-action-gates',
|
|
251
|
+
title: 'How to Stop AI Coding Agents From Repeating Mistakes | ThumbGate',
|
|
252
|
+
heroTitle: 'How to Stop AI Coding Agents From Repeating Mistakes',
|
|
253
|
+
heroSummary: 'If your agent keeps repeating the same bad move, the fix is not more memory alone. The fix is a feedback loop that turns repeated failures into pre-action gates before the next tool call executes.',
|
|
254
|
+
takeaways: [
|
|
255
|
+
'Repeated mistakes are a workflow problem, not just a context-window problem.',
|
|
256
|
+
'ThumbGate turns thumbs-down feedback into prevention rules and runtime gates.',
|
|
257
|
+
'This page is meant to move problem-aware buyers into the Pro path or a concrete install.',
|
|
258
|
+
],
|
|
259
|
+
sections: [
|
|
260
|
+
{
|
|
261
|
+
heading: 'Why repeated mistakes keep happening',
|
|
262
|
+
paragraphs: [
|
|
263
|
+
'AI coding agents are fast, but they forget operational pain surprisingly easily. One bad deployment, force-push, or skipped verification step often turns into another because the system remembered the transcript but never enforced the lesson.',
|
|
264
|
+
'That is why teams feel stuck in a correction loop. They keep teaching the same rule, but the next session still allows the same risky action.',
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
heading: 'What changes when feedback becomes enforcement',
|
|
269
|
+
bullets: [
|
|
270
|
+
'Thumbs down captures the exact failure you do not want repeated.',
|
|
271
|
+
'Repeated failures promote into linked prevention rules.',
|
|
272
|
+
'Pre-action gates intercept the risky tool call before execution.',
|
|
273
|
+
'Thumbs up reinforces the safe path so the agent learns what good looks like too.',
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
heading: 'What a buyer should do next',
|
|
278
|
+
paragraphs: [
|
|
279
|
+
'If the pain is already real, do not start with a long architecture project. Start by wiring ThumbGate into the workflow where the agent has already burned time or trust, then watch the next repeat attempt get blocked before damage lands.',
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
faq: [
|
|
284
|
+
{
|
|
285
|
+
question: 'Is memory alone enough to stop repeated mistakes?',
|
|
286
|
+
answer: 'Usually no. Memory helps retrieval, but ThumbGate adds pre-action gates so the same risky move can be blocked before the next command executes.',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
question: 'Does ThumbGate only punish bad behavior?',
|
|
290
|
+
answer: 'No. Thumbs up reinforces good behavior, so the loop captures safe patterns as well as failures.',
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
relatedPaths: ['/guides/pre-action-gates', '/guides/claude-code-feedback'],
|
|
294
|
+
},
|
|
228
295
|
{
|
|
229
296
|
query: 'claude code feedback memory',
|
|
230
297
|
path: '/guides/claude-code-feedback',
|
|
@@ -273,6 +340,150 @@ const PAGE_BLUEPRINTS = [
|
|
|
273
340
|
],
|
|
274
341
|
relatedPaths: ['/guides/pre-action-gates', '/compare/mem0'],
|
|
275
342
|
},
|
|
343
|
+
{
|
|
344
|
+
query: 'cursor prevent repeated mistakes',
|
|
345
|
+
path: '/guides/cursor-agent-guardrails',
|
|
346
|
+
pageType: 'integration',
|
|
347
|
+
pillar: 'agent-workflows',
|
|
348
|
+
title: 'Cursor Agent Guardrails | Stop Repeated Mistakes with ThumbGate',
|
|
349
|
+
heroTitle: 'Cursor Guardrails That Block Repeated Mistakes',
|
|
350
|
+
heroSummary: 'Cursor moves fast, which makes repeated mistakes expensive. ThumbGate gives Cursor users a feedback loop that turns thumbs-down corrections into pre-action gates before the next risky step fires.',
|
|
351
|
+
takeaways: [
|
|
352
|
+
'Cursor users want speed without trusting the agent blindly.',
|
|
353
|
+
'ThumbGate adds enforcement without forcing a platform switch.',
|
|
354
|
+
'The page should answer the buyer question in one line: how do I stop Cursor from doing the same bad thing again?',
|
|
355
|
+
],
|
|
356
|
+
sections: [
|
|
357
|
+
{
|
|
358
|
+
heading: 'The Cursor workflow problem',
|
|
359
|
+
paragraphs: [
|
|
360
|
+
'Cursor can move from idea to edits quickly, but the failure mode is familiar: the same wrong refactor, risky shell command, or skipped check comes back in the next session because nothing hardened the workflow.',
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
heading: 'How ThumbGate fits into Cursor',
|
|
365
|
+
bullets: [
|
|
366
|
+
'Capture thumbs-up/down feedback on agent behavior.',
|
|
367
|
+
'Promote repeated failures into prevention rules.',
|
|
368
|
+
'Block known-bad commands with pre-action gates before execution.',
|
|
369
|
+
'Keep the memory and gates local-first so the operator retains control.',
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
heading: 'What makes this different from a rule file',
|
|
374
|
+
paragraphs: [
|
|
375
|
+
'Static rules help on day one. ThumbGate helps on day two and day twenty because it keeps learning from live corrections instead of relying on a fixed checklist that drifts out of date.',
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
faq: [
|
|
380
|
+
{
|
|
381
|
+
question: 'Do I need to leave Cursor to use ThumbGate?',
|
|
382
|
+
answer: 'No. ThumbGate is designed to sit alongside existing coding-agent workflows so you can add enforcement without switching tools.',
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
question: 'What kind of mistakes can Cursor guardrails stop?',
|
|
386
|
+
answer: 'Repeated failures like risky git actions, destructive scripts, skipped verification, or any other known-bad pattern you have already corrected once.',
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
relatedPaths: ['/guides/stop-repeated-ai-agent-mistakes', '/guides/pre-action-gates'],
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
query: 'codex cli guardrails',
|
|
393
|
+
path: '/guides/codex-cli-guardrails',
|
|
394
|
+
pageType: 'integration',
|
|
395
|
+
pillar: 'agent-workflows',
|
|
396
|
+
title: 'Codex CLI Guardrails | Prevent Repeated Mistakes with ThumbGate',
|
|
397
|
+
heroTitle: 'Codex CLI Guardrails That Actually Enforce',
|
|
398
|
+
heroSummary: 'Codex CLI can move quickly through repo tasks, but buyers need more than good intentions. ThumbGate adds a reliability gateway so repeated mistakes become searchable lessons, linked rules, and pre-action enforcement.',
|
|
399
|
+
takeaways: [
|
|
400
|
+
'Codex CLI buyers are usually looking for safe autonomy, not just more prompts.',
|
|
401
|
+
'ThumbGate sits in the critical gap between feedback and execution.',
|
|
402
|
+
'This page should rank for people who want guardrails without giving up CLI speed.',
|
|
403
|
+
],
|
|
404
|
+
sections: [
|
|
405
|
+
{
|
|
406
|
+
heading: 'What Codex CLI users usually need',
|
|
407
|
+
paragraphs: [
|
|
408
|
+
'The problem is rarely a single bad command. It is the cost of the same failure pattern showing up across branches, sessions, or rushed workflows. Once that pattern is obvious, the buyer wants a durable control point.',
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
heading: 'What ThumbGate adds',
|
|
413
|
+
bullets: [
|
|
414
|
+
'Feedback capture with explicit thumbs-up/down signals.',
|
|
415
|
+
'Searchable lessons and linked prevention rules.',
|
|
416
|
+
'Pre-action gates that block repeated bad commands before they run.',
|
|
417
|
+
'Verification evidence that gives teams something concrete to audit.',
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
heading: 'Why this matters for revenue',
|
|
422
|
+
paragraphs: [
|
|
423
|
+
'Guardrails are easier to buy when the outcome is obvious: less rework, fewer repeated failures, and a visible chain from operator feedback to enforced behavior.',
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
faq: [
|
|
428
|
+
{
|
|
429
|
+
question: 'Is ThumbGate only for Codex CLI?',
|
|
430
|
+
answer: 'No. Codex CLI is one supported workflow, but the same feedback and enforcement loop also works across Claude Code, Cursor, Gemini, Amp, and OpenCode.',
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
question: 'How are Codex CLI guardrails different from prompt instructions?',
|
|
434
|
+
answer: 'Prompt instructions are advisory. ThumbGate pre-action gates intercept the tool call itself and block the known-bad pattern before execution.',
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
relatedPaths: ['/guides/pre-action-gates', '/compare/mem0'],
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
query: 'gemini cli feedback memory',
|
|
441
|
+
path: '/guides/gemini-cli-feedback-memory',
|
|
442
|
+
pageType: 'integration',
|
|
443
|
+
pillar: 'agent-workflows',
|
|
444
|
+
title: 'Gemini CLI Feedback Memory | Memory Plus Enforcement with ThumbGate',
|
|
445
|
+
heroTitle: 'Gemini CLI Feedback Memory That Leads to Enforcement',
|
|
446
|
+
heroSummary: 'Gemini CLI users often start by asking for better memory. ThumbGate answers the bigger need: memory that can become prevention rules and pre-action gates when the same mistake shows up twice.',
|
|
447
|
+
takeaways: [
|
|
448
|
+
'Gemini CLI searchers often begin with memory but buy because of enforcement.',
|
|
449
|
+
'ThumbGate keeps the local-first memory story while adding runtime blocking.',
|
|
450
|
+
'The ideal conversion path here is memory query to product proof to Pro page.',
|
|
451
|
+
],
|
|
452
|
+
sections: [
|
|
453
|
+
{
|
|
454
|
+
heading: 'Why memory is only step one',
|
|
455
|
+
paragraphs: [
|
|
456
|
+
'Persistent memory helps Gemini CLI recall past context, but it still leaves a blind spot. Remembering that a workflow went badly is different from preventing the next risky action when the same pattern appears again.',
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
heading: 'What ThumbGate adds on top',
|
|
461
|
+
bullets: [
|
|
462
|
+
'Local-first lessons you can search across sessions.',
|
|
463
|
+
'Structured thumbs-up/down feedback for reinforcement and correction.',
|
|
464
|
+
'Prevention rules linked to past failures.',
|
|
465
|
+
'Pre-action gates that stop repeated mistakes before execution.',
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
heading: 'Who this is really for',
|
|
470
|
+
paragraphs: [
|
|
471
|
+
'This page is for operators who already know memory matters, but now need a reliability layer that protects live workflows instead of just preserving notes about them.',
|
|
472
|
+
],
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
faq: [
|
|
476
|
+
{
|
|
477
|
+
question: 'Does ThumbGate replace Gemini CLI memory?',
|
|
478
|
+
answer: 'No. ThumbGate extends the memory story with searchable lessons, rules, and gates so memory becomes operationally useful instead of purely historical.',
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
question: 'Can this stay local-first?',
|
|
482
|
+
answer: 'Yes. ThumbGate is built for local-first workflows, which lowers risk for developers who do not want sensitive history pushed into a hosted memory layer.',
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
relatedPaths: ['/compare/mem0', '/guides/stop-repeated-ai-agent-mistakes'],
|
|
486
|
+
},
|
|
276
487
|
{
|
|
277
488
|
query: 'claude desktop extension plugin thumbgate',
|
|
278
489
|
path: '/guides/claude-desktop',
|
|
@@ -444,7 +655,7 @@ function classifyIntent(query) {
|
|
|
444
655
|
return 'commercial';
|
|
445
656
|
}
|
|
446
657
|
if (/\b(what is|how to|guide|best practices|why)\b/.test(normalized)) return 'informational';
|
|
447
|
-
if (/\b(guardrails|pre-action gates|feedback|prevent repeated mistakes|memory)\b/.test(normalized)) {
|
|
658
|
+
if (/\b(guardrails|pre-action gates|feedback|prevent repeated mistakes|repeating mistakes|memory)\b/.test(normalized)) {
|
|
448
659
|
return 'commercial';
|
|
449
660
|
}
|
|
450
661
|
return 'informational';
|
|
@@ -454,7 +665,7 @@ function inferPillar(query) {
|
|
|
454
665
|
const normalized = normalizeText(query).toLowerCase();
|
|
455
666
|
if (/\b(speclock|mem0|alternative|vs|compare|comparison)\b/.test(normalized)) return 'comparison';
|
|
456
667
|
if (/\b(thumbs up|thumbs down|feedback|reinforce|mistake)\b/.test(normalized)) return 'feedback-loop';
|
|
457
|
-
if (/\b(pre-action gates|guardrails|block|prevent repeated mistakes)\b/.test(normalized)) return 'pre-action-gates';
|
|
668
|
+
if (/\b(pre-action gates|guardrails|block|prevent repeated mistakes|repeating mistakes)\b/.test(normalized)) return 'pre-action-gates';
|
|
458
669
|
if (/\b(claude code|cursor|codex|gemini|amp|opencode|integration|plugin)\b/.test(normalized)) return 'agent-workflows';
|
|
459
670
|
return 'ai-agent-reliability';
|
|
460
671
|
}
|
|
@@ -463,6 +674,8 @@ function inferPersona(query) {
|
|
|
463
674
|
const normalized = normalizeText(query).toLowerCase();
|
|
464
675
|
if (normalized.includes('claude code')) return 'claude-code-builder';
|
|
465
676
|
if (normalized.includes('cursor')) return 'cursor-builder';
|
|
677
|
+
if (normalized.includes('codex')) return 'codex-builder';
|
|
678
|
+
if (normalized.includes('gemini')) return 'gemini-builder';
|
|
466
679
|
if (/\b(vs|alternative|compare)\b/.test(normalized)) return 'tool-evaluator';
|
|
467
680
|
if (/\b(guardrails|pre-action gates)\b/.test(normalized)) return 'engineering-lead';
|
|
468
681
|
return 'ai-engineer';
|
|
@@ -628,8 +841,8 @@ function createPageSpec(blueprint, row) {
|
|
|
628
841
|
faq: blueprint.faq,
|
|
629
842
|
relatedPages,
|
|
630
843
|
cta: {
|
|
631
|
-
label: '
|
|
632
|
-
href:
|
|
844
|
+
label: 'See ThumbGate Pro',
|
|
845
|
+
href: `/pro?utm_source=website&utm_medium=seo_page&utm_campaign=${blueprint.path.split('/').filter(Boolean).join('_')}`,
|
|
633
846
|
},
|
|
634
847
|
proofLinks: [
|
|
635
848
|
{ label: 'Verification evidence', href: PRODUCT.verificationUrl },
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
-
|
|
5
|
+
let dotenv = null;
|
|
6
|
+
try {
|
|
7
|
+
dotenv = require('dotenv');
|
|
8
|
+
} catch (_) {
|
|
9
|
+
dotenv = null;
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
const DEFAULT_ENV_PATH = path.resolve(__dirname, '..', '..', '.env');
|
|
8
13
|
|
|
@@ -20,7 +25,8 @@ function loadLocalEnv(options = {}) {
|
|
|
20
25
|
};
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
const
|
|
28
|
+
const source = fs.readFileSync(resolvedPath, 'utf8');
|
|
29
|
+
const parsed = dotenv ? dotenv.parse(source) : parseEnvFallback(source);
|
|
24
30
|
const loadedKeys = [];
|
|
25
31
|
const override = options.override === true;
|
|
26
32
|
|
|
@@ -39,8 +45,33 @@ function loadLocalEnv(options = {}) {
|
|
|
39
45
|
};
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
function parseEnvFallback(source) {
|
|
49
|
+
const parsed = {};
|
|
50
|
+
for (const rawLine of String(source).split(/\r?\n/)) {
|
|
51
|
+
const line = rawLine.trim();
|
|
52
|
+
if (!line || line.startsWith('#')) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const separatorIndex = line.indexOf('=');
|
|
56
|
+
if (separatorIndex <= 0) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
60
|
+
let value = line.slice(separatorIndex + 1).trim();
|
|
61
|
+
if (
|
|
62
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
63
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
64
|
+
) {
|
|
65
|
+
value = value.slice(1, -1);
|
|
66
|
+
}
|
|
67
|
+
parsed[key] = value;
|
|
68
|
+
}
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
module.exports = {
|
|
43
73
|
DEFAULT_ENV_PATH,
|
|
44
74
|
loadLocalEnv,
|
|
75
|
+
parseEnvFallback,
|
|
45
76
|
resolveEnvPath,
|
|
46
77
|
};
|
|
@@ -2,11 +2,209 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
-
|
|
5
|
+
let Database = null;
|
|
6
|
+
try {
|
|
7
|
+
Database = require('better-sqlite3');
|
|
8
|
+
} catch (_) {
|
|
9
|
+
Database = null;
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
const DEFAULT_DB_PATH = path.join(__dirname, 'db', 'social-analytics.db');
|
|
8
13
|
const SCHEMA_PATH = path.join(__dirname, 'db', 'schema.sql');
|
|
9
14
|
|
|
15
|
+
class MemoryStatement {
|
|
16
|
+
constructor(handler) {
|
|
17
|
+
this.handler = handler;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
run(params) {
|
|
21
|
+
return this.handler.run(params);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
all(params) {
|
|
25
|
+
return this.handler.all(params);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class MemoryDatabase {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.tables = {
|
|
32
|
+
engagement_metrics: [],
|
|
33
|
+
follower_snapshots: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pragma() {}
|
|
38
|
+
|
|
39
|
+
exec() {}
|
|
40
|
+
|
|
41
|
+
close() {}
|
|
42
|
+
|
|
43
|
+
prepare(sql) {
|
|
44
|
+
const normalized = sql.replace(/\s+/g, ' ').trim();
|
|
45
|
+
|
|
46
|
+
if (normalized.includes('INSERT OR REPLACE INTO engagement_metrics')) {
|
|
47
|
+
return new MemoryStatement({
|
|
48
|
+
run: (params) => {
|
|
49
|
+
const index = this.tables.engagement_metrics.findIndex(
|
|
50
|
+
(row) =>
|
|
51
|
+
row.platform === params.platform &&
|
|
52
|
+
row.post_id === params.post_id &&
|
|
53
|
+
row.metric_date === params.metric_date
|
|
54
|
+
);
|
|
55
|
+
const row = { ...params };
|
|
56
|
+
if (index >= 0) {
|
|
57
|
+
this.tables.engagement_metrics[index] = row;
|
|
58
|
+
} else {
|
|
59
|
+
this.tables.engagement_metrics.push(row);
|
|
60
|
+
}
|
|
61
|
+
return { changes: 1 };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (normalized.includes('INSERT OR REPLACE INTO follower_snapshots')) {
|
|
67
|
+
return new MemoryStatement({
|
|
68
|
+
run: (params) => {
|
|
69
|
+
const index = this.tables.follower_snapshots.findIndex(
|
|
70
|
+
(row) => row.platform === params.platform && row.snapshot_date === params.snapshot_date
|
|
71
|
+
);
|
|
72
|
+
const row = { ...params };
|
|
73
|
+
if (index >= 0) {
|
|
74
|
+
this.tables.follower_snapshots[index] = row;
|
|
75
|
+
} else {
|
|
76
|
+
this.tables.follower_snapshots.push(row);
|
|
77
|
+
}
|
|
78
|
+
return { changes: 1 };
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (normalized.includes('SELECT * FROM engagement_metrics WHERE platform = ?')) {
|
|
84
|
+
return new MemoryStatement({
|
|
85
|
+
all: (platform) =>
|
|
86
|
+
this.tables.engagement_metrics.filter((row) => row.platform === platform),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const literalPlatformMatch = normalized.match(
|
|
91
|
+
/^SELECT \* FROM engagement_metrics WHERE platform = '([^']+)'$/i
|
|
92
|
+
);
|
|
93
|
+
if (literalPlatformMatch) {
|
|
94
|
+
const [, platform] = literalPlatformMatch;
|
|
95
|
+
return new MemoryStatement({
|
|
96
|
+
all: () =>
|
|
97
|
+
this.tables.engagement_metrics.filter((row) => row.platform === platform),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (normalized.includes('FROM engagement_metrics') && normalized.includes('GROUP BY platform')) {
|
|
102
|
+
return new MemoryStatement({
|
|
103
|
+
all: (params = {}) => {
|
|
104
|
+
const rows = this.tables.engagement_metrics.filter((row) => {
|
|
105
|
+
if (row.metric_date < params.cutoff) return false;
|
|
106
|
+
if (params.platform && row.platform !== params.platform) return false;
|
|
107
|
+
return true;
|
|
108
|
+
});
|
|
109
|
+
const grouped = new Map();
|
|
110
|
+
for (const row of rows) {
|
|
111
|
+
const bucket = grouped.get(row.platform) || {
|
|
112
|
+
platform: row.platform,
|
|
113
|
+
post_count: 0,
|
|
114
|
+
total_impressions: 0,
|
|
115
|
+
total_reach: 0,
|
|
116
|
+
total_likes: 0,
|
|
117
|
+
total_comments: 0,
|
|
118
|
+
total_shares: 0,
|
|
119
|
+
total_saves: 0,
|
|
120
|
+
total_clicks: 0,
|
|
121
|
+
total_video_views: 0,
|
|
122
|
+
_postIds: new Set(),
|
|
123
|
+
_rows: 0,
|
|
124
|
+
};
|
|
125
|
+
bucket._postIds.add(row.post_id);
|
|
126
|
+
bucket.total_impressions += row.impressions || 0;
|
|
127
|
+
bucket.total_reach += row.reach || 0;
|
|
128
|
+
bucket.total_likes += row.likes || 0;
|
|
129
|
+
bucket.total_comments += row.comments || 0;
|
|
130
|
+
bucket.total_shares += row.shares || 0;
|
|
131
|
+
bucket.total_saves += row.saves || 0;
|
|
132
|
+
bucket.total_clicks += row.clicks || 0;
|
|
133
|
+
bucket.total_video_views += row.video_views || 0;
|
|
134
|
+
bucket._rows += 1;
|
|
135
|
+
grouped.set(row.platform, bucket);
|
|
136
|
+
}
|
|
137
|
+
return [...grouped.values()]
|
|
138
|
+
.map((bucket) => ({
|
|
139
|
+
platform: bucket.platform,
|
|
140
|
+
post_count: bucket._postIds.size,
|
|
141
|
+
total_impressions: bucket.total_impressions,
|
|
142
|
+
total_reach: bucket.total_reach,
|
|
143
|
+
total_likes: bucket.total_likes,
|
|
144
|
+
total_comments: bucket.total_comments,
|
|
145
|
+
total_shares: bucket.total_shares,
|
|
146
|
+
total_saves: bucket.total_saves,
|
|
147
|
+
total_clicks: bucket.total_clicks,
|
|
148
|
+
total_video_views: bucket.total_video_views,
|
|
149
|
+
avg_impressions: Number((bucket.total_impressions / bucket._rows).toFixed(2)),
|
|
150
|
+
avg_likes: Number((bucket.total_likes / bucket._rows).toFixed(2)),
|
|
151
|
+
}))
|
|
152
|
+
.sort((a, b) => b.total_impressions - a.total_impressions);
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (normalized.includes('FROM engagement_metrics') && normalized.includes('GROUP BY platform, post_id')) {
|
|
158
|
+
return new MemoryStatement({
|
|
159
|
+
all: ({ cutoff, limit }) => {
|
|
160
|
+
const rows = this.tables.engagement_metrics.filter((row) => row.metric_date >= cutoff);
|
|
161
|
+
const grouped = new Map();
|
|
162
|
+
for (const row of rows) {
|
|
163
|
+
const key = `${row.platform}::${row.post_id}`;
|
|
164
|
+
const bucket = grouped.get(key) || {
|
|
165
|
+
platform: row.platform,
|
|
166
|
+
content_type: row.content_type,
|
|
167
|
+
post_id: row.post_id,
|
|
168
|
+
post_url: row.post_url ?? null,
|
|
169
|
+
published_at: row.published_at ?? null,
|
|
170
|
+
total_engagement: 0,
|
|
171
|
+
total_impressions: 0,
|
|
172
|
+
total_likes: 0,
|
|
173
|
+
total_comments: 0,
|
|
174
|
+
total_shares: 0,
|
|
175
|
+
total_saves: 0,
|
|
176
|
+
total_video_views: 0,
|
|
177
|
+
};
|
|
178
|
+
bucket.total_engagement +=
|
|
179
|
+
(row.likes || 0) + (row.comments || 0) + (row.shares || 0) + (row.saves || 0);
|
|
180
|
+
bucket.total_impressions += row.impressions || 0;
|
|
181
|
+
bucket.total_likes += row.likes || 0;
|
|
182
|
+
bucket.total_comments += row.comments || 0;
|
|
183
|
+
bucket.total_shares += row.shares || 0;
|
|
184
|
+
bucket.total_saves += row.saves || 0;
|
|
185
|
+
bucket.total_video_views += row.video_views || 0;
|
|
186
|
+
grouped.set(key, bucket);
|
|
187
|
+
}
|
|
188
|
+
return [...grouped.values()]
|
|
189
|
+
.sort((a, b) => b.total_engagement - a.total_engagement)
|
|
190
|
+
.slice(0, limit);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (normalized.includes('FROM follower_snapshots')) {
|
|
196
|
+
return new MemoryStatement({
|
|
197
|
+
all: ({ platform, cutoff }) =>
|
|
198
|
+
this.tables.follower_snapshots
|
|
199
|
+
.filter((row) => row.platform === platform && row.snapshot_date >= cutoff)
|
|
200
|
+
.sort((a, b) => a.snapshot_date.localeCompare(b.snapshot_date)),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
throw new Error(`MemoryDatabase does not support query: ${normalized}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
10
208
|
/**
|
|
11
209
|
* Opens the SQLite database, applies the schema, and returns the db instance.
|
|
12
210
|
* Idempotent — safe to call multiple times; schema uses IF NOT EXISTS guards.
|
|
@@ -23,7 +221,7 @@ function initDb(dbPath = DEFAULT_DB_PATH) {
|
|
|
23
221
|
fs.mkdirSync(dir, { recursive: true });
|
|
24
222
|
}
|
|
25
223
|
|
|
26
|
-
const db = new Database(resolvedPath);
|
|
224
|
+
const db = Database ? new Database(resolvedPath) : new MemoryDatabase();
|
|
27
225
|
db.pragma('busy_timeout = 3000');
|
|
28
226
|
|
|
29
227
|
// Enable WAL mode for better concurrent read performance.
|
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
listFeedbackArtifactPaths,
|
|
7
|
+
resolveFeedbackDir,
|
|
8
|
+
resolveProjectDir,
|
|
9
|
+
} = require('./feedback-paths');
|
|
6
10
|
|
|
7
11
|
function unique(values = []) {
|
|
8
12
|
return [...new Set(values.filter(Boolean).map((value) => path.resolve(value)))];
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
function getStatuslineCacheCandidates(options = {}) {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const feedbackDir = resolveFeedbackDir({
|
|
16
|
+
const env = options.env || process.env;
|
|
17
|
+
const projectDir = resolveProjectDir({ cwd: options.cwd, env });
|
|
18
|
+
const feedbackDir = resolveFeedbackDir({ projectDir, env });
|
|
15
19
|
|
|
16
20
|
return unique([
|
|
21
|
+
...listFeedbackArtifactPaths('statusline_cache.json', { projectDir, env }),
|
|
17
22
|
path.join(feedbackDir, 'statusline_cache.json'),
|
|
18
|
-
path.join(cwd, '.thumbgate', 'statusline_cache.json'),
|
|
19
|
-
home ? path.join(home, '.thumbgate', 'statusline_cache.json') : null,
|
|
20
23
|
]);
|
|
21
24
|
}
|
|
22
25
|
|
package/scripts/sync-version.js
CHANGED
|
@@ -18,6 +18,7 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
|
|
20
20
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
21
|
+
const VERSION_PATTERN = '\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?';
|
|
21
22
|
|
|
22
23
|
function explicitPinnedServeArgs(version) {
|
|
23
24
|
return ['--yes', '--package', `thumbgate@${version}`, 'thumbgate', 'serve'];
|
|
@@ -308,7 +309,7 @@ function syncVersion(opts) {
|
|
|
308
309
|
'plugins/codex-profile/INSTALL.md',
|
|
309
310
|
'plugins/opencode-profile/INSTALL.md',
|
|
310
311
|
];
|
|
311
|
-
const pinnedPackagePattern =
|
|
312
|
+
const pinnedPackagePattern = new RegExp(`thumbgate@${VERSION_PATTERN}`, 'g');
|
|
312
313
|
for (const relPath of pinnedPackageTargets) {
|
|
313
314
|
const filePath = path.join(PROJECT_ROOT, relPath);
|
|
314
315
|
if (!fs.existsSync(filePath)) continue;
|
|
@@ -329,7 +330,7 @@ function syncVersion(opts) {
|
|
|
329
330
|
if (fs.existsSync(path.join(PROJECT_ROOT, landingPath))) {
|
|
330
331
|
const landingContent = fs.readFileSync(path.join(PROJECT_ROOT, landingPath), 'utf-8');
|
|
331
332
|
// Match any version pattern in the hero badge
|
|
332
|
-
const badgeMatch = landingContent.match(
|
|
333
|
+
const badgeMatch = landingContent.match(new RegExp(`v(${VERSION_PATTERN}) — Hosted API`));
|
|
333
334
|
if (badgeMatch && badgeMatch[1] !== version) {
|
|
334
335
|
drifted.push({ file: landingPath, field: 'hero-badge', current: badgeMatch[1] });
|
|
335
336
|
if (!checkOnly) {
|
|
@@ -337,7 +338,7 @@ function syncVersion(opts) {
|
|
|
337
338
|
}
|
|
338
339
|
}
|
|
339
340
|
// JSON snippet version
|
|
340
|
-
const jsonMatch = landingContent.match(
|
|
341
|
+
const jsonMatch = landingContent.match(new RegExp(`"version"<\\/span><span class="out">: <\\/span><span class="val">"(${VERSION_PATTERN})"`));
|
|
341
342
|
if (jsonMatch && jsonMatch[1] !== version) {
|
|
342
343
|
drifted.push({ file: landingPath, field: 'json-snippet', current: jsonMatch[1] });
|
|
343
344
|
if (!checkOnly) {
|
|
@@ -351,7 +352,7 @@ function syncVersion(opts) {
|
|
|
351
352
|
const mcpSubmPath = 'docs/mcp-hub-submission.md';
|
|
352
353
|
if (fs.existsSync(path.join(PROJECT_ROOT, mcpSubmPath))) {
|
|
353
354
|
const mcpContent = fs.readFileSync(path.join(PROJECT_ROOT, mcpSubmPath), 'utf-8');
|
|
354
|
-
const versionMatch = mcpContent.match(
|
|
355
|
+
const versionMatch = mcpContent.match(new RegExp(`## Version\\s+(${VERSION_PATTERN})`));
|
|
355
356
|
if (versionMatch && versionMatch[1] !== version) {
|
|
356
357
|
drifted.push({ file: mcpSubmPath, field: 'version-heading', current: versionMatch[1] });
|
|
357
358
|
if (!checkOnly) {
|
|
@@ -366,18 +367,18 @@ function syncVersion(opts) {
|
|
|
366
367
|
if (fs.existsSync(path.join(PROJECT_ROOT, publicIndexPath))) {
|
|
367
368
|
const publicIndexFile = path.join(PROJECT_ROOT, publicIndexPath);
|
|
368
369
|
const publicContent = fs.readFileSync(publicIndexFile, 'utf-8');
|
|
369
|
-
const heroVersionMatch = publicContent.match(
|
|
370
|
+
const heroVersionMatch = publicContent.match(new RegExp(`New in v(${VERSION_PATTERN}):?`));
|
|
370
371
|
if (heroVersionMatch && heroVersionMatch[1] !== version) {
|
|
371
372
|
drifted.push({ file: publicIndexPath, field: 'hero-release-note', current: heroVersionMatch[1] });
|
|
372
373
|
if (!checkOnly) {
|
|
373
374
|
fs.writeFileSync(
|
|
374
375
|
publicIndexFile,
|
|
375
|
-
publicContent.replace(
|
|
376
|
+
publicContent.replace(new RegExp(`New in v${VERSION_PATTERN}:?`), `New in v${version}`)
|
|
376
377
|
);
|
|
377
378
|
}
|
|
378
379
|
}
|
|
379
380
|
|
|
380
|
-
const proofMatch = publicContent.match(
|
|
381
|
+
const proofMatch = publicContent.match(new RegExp(`Versioned proof: v(${VERSION_PATTERN})`));
|
|
381
382
|
if (proofMatch && proofMatch[1] !== version) {
|
|
382
383
|
drifted.push({ file: publicIndexPath, field: 'proof-pill', current: proofMatch[1] });
|
|
383
384
|
if (!checkOnly) {
|
|
@@ -385,11 +386,17 @@ function syncVersion(opts) {
|
|
|
385
386
|
}
|
|
386
387
|
}
|
|
387
388
|
|
|
388
|
-
const footerMatch = publicContent.match(
|
|
389
|
+
const footerMatch = publicContent.match(new RegExp(`(?:Context Gateway|MIT License) [•·] v(${VERSION_PATTERN})`));
|
|
389
390
|
if (footerMatch && footerMatch[1] !== version) {
|
|
390
391
|
drifted.push({ file: publicIndexPath, field: 'footer-version', current: footerMatch[1] });
|
|
391
392
|
if (!checkOnly) {
|
|
392
|
-
|
|
393
|
+
fs.writeFileSync(
|
|
394
|
+
publicIndexFile,
|
|
395
|
+
publicContent.replace(
|
|
396
|
+
new RegExp(`((?:Context Gateway|MIT License) [•·] )v${VERSION_PATTERN}`),
|
|
397
|
+
`$1v${version}`
|
|
398
|
+
)
|
|
399
|
+
);
|
|
393
400
|
}
|
|
394
401
|
}
|
|
395
402
|
targets.push(publicIndexPath);
|
|
@@ -400,13 +407,13 @@ function syncVersion(opts) {
|
|
|
400
407
|
const serverStdioFile = path.join(PROJECT_ROOT, serverStdioPath);
|
|
401
408
|
if (fs.existsSync(serverStdioFile)) {
|
|
402
409
|
const serverStdioContent = fs.readFileSync(serverStdioFile, 'utf-8');
|
|
403
|
-
const serverInfoMatch = serverStdioContent.match(
|
|
410
|
+
const serverInfoMatch = serverStdioContent.match(new RegExp(`version:\\s*'(${VERSION_PATTERN})'`));
|
|
404
411
|
if (serverInfoMatch && serverInfoMatch[1] !== version) {
|
|
405
412
|
drifted.push({ file: serverStdioPath, field: 'server-info-version', current: serverInfoMatch[1] });
|
|
406
413
|
if (!checkOnly) {
|
|
407
414
|
fs.writeFileSync(
|
|
408
415
|
serverStdioFile,
|
|
409
|
-
serverStdioContent.replace(
|
|
416
|
+
serverStdioContent.replace(new RegExp(`version:\\s*'${VERSION_PATTERN}'`), `version: '${version}'`)
|
|
410
417
|
);
|
|
411
418
|
}
|
|
412
419
|
}
|